refactor, restructure typescript types

This commit is contained in:
thatmattlove 2021-12-18 23:45:06 -07:00
parent 47711fb075
commit 93adc09e63
95 changed files with 634 additions and 767 deletions

View file

@ -1,9 +1,13 @@
import { Flex } from '@chakra-ui/react';
import { useColorValue } from '~/context';
import type { TCardBody } from './types';
import type { FlexProps } from '@chakra-ui/react';
export const CardBody = (props: TCardBody): JSX.Element => {
interface CardBodyProps extends Omit<FlexProps, 'onClick'> {
onClick?: () => boolean;
}
export const CardBody = (props: CardBodyProps): JSX.Element => {
const { onClick, ...rest } = props;
const bg = useColorValue('white', 'dark.500');
const color = useColorValue('dark.500', 'white');

View file

@ -1,8 +1,8 @@
import { Flex } from '@chakra-ui/react';
import type { TCardFooter } from './types';
import type { FlexProps } from '@chakra-ui/react';
export const CardFooter = (props: TCardFooter): JSX.Element => (
export const CardFooter = (props: FlexProps): JSX.Element => (
<Flex
p={4}
direction="column"

View file

@ -1,9 +1,9 @@
import { Flex, Text } from '@chakra-ui/react';
import { useColorValue } from '~/context';
import type { TCardHeader } from './types';
import type { FlexProps } from '@chakra-ui/react';
export const CardHeader = (props: TCardHeader): JSX.Element => {
export const CardHeader = (props: FlexProps): JSX.Element => {
const { children, ...rest } = props;
const bg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
return (

View file

@ -1,9 +0,0 @@
import type { FlexProps } from '@chakra-ui/react';
export interface TCardBody extends Omit<FlexProps, 'onClick'> {
onClick?: () => boolean;
}
export interface TCardFooter extends FlexProps {}
export interface TCardHeader extends FlexProps {}

View file

@ -3,9 +3,18 @@ import ReactCountdown, { zeroPad } from 'react-countdown';
import { If, Then, Else } from 'react-if';
import { useColorValue } from '~/context';
import type { TCountdown, TRenderer } from './types';
import type { CountdownRenderProps } from 'react-countdown';
const Renderer = (props: TRenderer): JSX.Element => {
interface RendererProps extends CountdownRenderProps {
text: string;
}
interface CountdownProps {
timeout: number;
text: string;
}
const Renderer = (props: RendererProps): JSX.Element => {
const { hours, minutes, seconds, completed, text } = props;
const time = [zeroPad(seconds)];
minutes !== 0 && time.unshift(zeroPad(minutes));
@ -28,7 +37,7 @@ const Renderer = (props: TRenderer): JSX.Element => {
);
};
export const Countdown = (props: TCountdown): JSX.Element => {
export const Countdown = (props: CountdownProps): JSX.Element => {
const { timeout, text } = props;
const then = timeout * 1000;
return (

View file

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

View file

@ -1,10 +0,0 @@
import type { CountdownRenderProps } from 'react-countdown';
export interface TRenderer extends CountdownRenderProps {
text: string;
}
export interface TCountdown {
timeout: number;
text: string;
}

View file

@ -4,8 +4,14 @@ import { Markdown } from '~/components';
import { useColorValue, useBreakpointValue, useConfig } from '~/context';
import { useOpposingColor, useStrf } from '~/hooks';
import type { MenuListProps } from '@chakra-ui/react';
import type { Config } from '~/types';
import type { TFooterButton } from './types';
interface FooterButtonProps extends Omit<MenuListProps, 'title'> {
side: 'left' | 'right';
title?: MenuListProps['children'];
content: string;
}
/**
* Filter the configuration object based on values that are strings for formatting.
@ -20,7 +26,7 @@ function getConfigFmt(config: Config): Record<string, string> {
return fmt;
}
export const FooterButton = (props: TFooterButton): JSX.Element => {
export const FooterButton = (props: FooterButtonProps): JSX.Element => {
const { content, title, side, ...rest } = props;
const config = useConfig();

View file

@ -5,10 +5,14 @@ import { DynamicIcon } from '~/components';
import { useColorMode, useColorValue, useBreakpointValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import type { TColorModeToggle } from './types';
import type { ButtonProps } from '@chakra-ui/react';
export const ColorModeToggle = forwardRef<HTMLButtonElement, TColorModeToggle>(
(props: TColorModeToggle, ref) => {
interface ColorModeToggleProps extends ButtonProps {
size?: string;
}
export const ColorModeToggle = forwardRef<HTMLButtonElement, ColorModeToggleProps>(
(props: ColorModeToggleProps, ref) => {
const { size = '1.5rem', ...rest } = props;
const { colorMode, toggleColorMode } = useColorMode();

View file

@ -5,9 +5,9 @@ import { DynamicIcon } from '~/components';
import { useConfig, useMobile, useColorValue, useBreakpointValue } from '~/context';
import { useStrf } from '~/hooks';
import { FooterButton } from './button';
import { ColorModeToggle } from './colorMode';
import { ColorModeToggle } from './color-mode';
import { FooterLink } from './link';
import { isLink, isMenu } from './types';
import { isLink, isMenu } from '~/types';
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
import type { Link, Menu } from '~/types';

View file

@ -1,8 +1,10 @@
import { Button, Link, useBreakpointValue } from '@chakra-ui/react';
import type { TFooterLink } from './types';
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
export const FooterLink = (props: TFooterLink): JSX.Element => {
type FooterLinkProps = ButtonProps & LinkProps & { title: string };
export const FooterLink = (props: FooterLinkProps): JSX.Element => {
const { title } = props;
const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' });
return (

View file

@ -1,26 +0,0 @@
import type { ButtonProps, LinkProps, MenuListProps } from '@chakra-ui/react';
import type { Link, Menu } from '~/types';
type TFooterSide = 'left' | 'right';
export interface TFooterButton extends Omit<MenuListProps, 'title'> {
side: TFooterSide;
title?: MenuListProps['children'];
content: string;
}
export type TFooterLink = ButtonProps & LinkProps & { title: string };
export type TFooterItems = 'help' | 'credit' | 'terms';
export interface TColorModeToggle extends ButtonProps {
size?: string;
}
export function isLink(item: Link | Menu): item is Link {
return 'url' in item;
}
export function isMenu(item: Link | Menu): item is Menu {
return 'content' in item;
}

View file

@ -5,11 +5,19 @@ import { If, Then } from 'react-if';
import { useColorValue } from '~/context';
import { useBooleanValue } from '~/hooks';
import type { FormControlProps } from '@chakra-ui/react';
import type { FieldError } from 'react-hook-form';
import type { FormData } from '~/types';
import type { TField } from './types';
export const FormField = (props: TField): JSX.Element => {
interface FormFieldProps extends FormControlProps {
name: string;
label: string;
hiddenLabels?: boolean;
labelAddOn?: React.ReactNode;
fieldAddOn?: React.ReactNode;
}
export const FormField = (props: FormFieldProps): JSX.Element => {
const { name, label, children, labelAddOn, fieldAddOn, hiddenLabels = false, ...rest } = props;
const labelColor = useColorValue('blackAlpha.700', 'whiteAlpha.700');
const errorColor = useColorValue('red.500', 'red.300');

View file

@ -1,6 +0,0 @@
export * from './row';
export * from './field';
export * from './queryType';
export * from './queryTarget';
export * from './queryLocation';
export * from './resolvedTarget';

View file

@ -1,40 +0,0 @@
import type { FormControlProps } from '@chakra-ui/react';
import type { UseFormRegister } from 'react-hook-form';
import type { OnChangeArgs, FormData, SingleOption } from '~/types';
export interface TField extends FormControlProps {
name: string;
label: string;
hiddenLabels?: boolean;
labelAddOn?: React.ReactNode;
fieldAddOn?: React.ReactNode;
}
export type OnChange = (f: OnChangeArgs) => void;
export interface TQuerySelectField {
onChange: OnChange;
label: string;
}
export interface TQueryTarget {
name: string;
placeholder: string;
register: UseFormRegister<FormData>;
onChange(e: OnChangeArgs): void;
}
export interface ResolvedTargetProps {
errorClose(): void;
}
export interface LocationCardProps {
option: SingleOption;
defaultChecked: boolean;
onChange(a: 'add' | 'remove', v: SingleOption): void;
hasError: boolean;
}
export interface UserIPProps {
setTarget(target: string): void;
}

View file

@ -14,9 +14,9 @@ import { Markdown } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { useGreeting, useOpposingColor } from '~/hooks';
import type { TGreeting } from './types';
import type { ModalContentProps } from '@chakra-ui/react';
export const Greeting = (props: TGreeting): JSX.Element => {
export const Greeting = (props: ModalContentProps): JSX.Element => {
const { web, content } = useConfig();
const { isAck, isOpen, open, ack } = useGreeting();

View file

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

View file

@ -1,3 +0,0 @@
import { BoxProps } from '@chakra-ui/react';
export interface TGreeting extends BoxProps {}

View file

@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react';
import { Image, Skeleton } from '@chakra-ui/react';
import { useColorValue, useConfig } from '~/context';
import type { TLogo } from './types';
import type { ImageProps } from '@chakra-ui/react';
/**
* Custom hook to handle loading the user's logo, errors loading the logo, and color mode changes.
@ -31,7 +31,7 @@ function useLogo(): [string, () => void] {
return useMemo(() => [fallback ?? src, setFallback], [fallback, setFallback, src]);
}
export const Logo = (props: TLogo): JSX.Element => {
export const Logo = (props: ImageProps): JSX.Element => {
const { web } = useConfig();
const { width } = web.logo;

View file

@ -4,11 +4,16 @@ import { isSafari } from 'react-device-detect';
import { Switch, Case } from 'react-if';
import { useConfig, useMobile } from '~/context';
import { useFormState, useFormInteractive } from '~/hooks';
import { SubtitleOnly } from './subtitleOnly';
import { TitleOnly } from './titleOnly';
import { Logo } from './logo';
import { SubtitleOnly } from './subtitle-only';
import { TitleOnly } from './title-only';
import type { TTitle, TTitleWrapper, TDWrapper, TMWrapper } from './types';
import type { FlexProps, StackProps } from '@chakra-ui/react';
import type { MotionProps } from 'framer-motion';
type DWrapperProps = Omit<StackProps, 'transition'> & MotionProps;
type MWrapperProps = Omit<StackProps, 'transition'> & MotionProps;
type TextOnlyProps = Partial<MotionProps & Omit<StackProps, 'transition'>>;
const AnimatedVStack = motion(VStack);
const AnimatedFlex = motion(Flex);
@ -16,7 +21,7 @@ const AnimatedFlex = motion(Flex);
/**
* Title wrapper for mobile devices, breakpoints sm & md.
*/
const MWrapper = (props: TMWrapper): JSX.Element => {
const MWrapper = (props: MWrapperProps): JSX.Element => {
const formInteractive = useFormInteractive();
return (
<AnimatedVStack
@ -31,7 +36,7 @@ const MWrapper = (props: TMWrapper): JSX.Element => {
/**
* Title wrapper for desktop devices, breakpoints lg & xl.
*/
const DWrapper = (props: TDWrapper): JSX.Element => {
const DWrapper = (props: DWrapperProps): JSX.Element => {
const formInteractive = useFormInteractive();
return (
<AnimatedVStack
@ -50,11 +55,15 @@ const DWrapper = (props: TDWrapper): JSX.Element => {
* Universal wrapper for title sub-components, which will be different depending on the
* `title_mode` configuration variable.
*/
const TitleWrapper = (props: TDWrapper | TMWrapper): JSX.Element => {
const TitleWrapper = (props: DWrapperProps | MWrapperProps): JSX.Element => {
const isMobile = useMobile();
return (
<>
{isMobile ? <MWrapper {...(props as TMWrapper)} /> : <DWrapper {...(props as TDWrapper)} />}
{isMobile ? (
<MWrapper {...(props as MWrapperProps)} />
) : (
<DWrapper {...(props as DWrapperProps)} />
)}
</>
);
};
@ -62,7 +71,7 @@ const TitleWrapper = (props: TDWrapper | TMWrapper): JSX.Element => {
/**
* Title sub-component if `title_mode` is set to `text_only`.
*/
const TextOnly = (props: TTitleWrapper): JSX.Element => {
const TextOnly = (props: TextOnlyProps): JSX.Element => {
return (
<TitleWrapper {...props}>
<TitleOnly />
@ -104,7 +113,7 @@ const All = (): JSX.Element => (
/**
* Title component which renders sub-components based on the `title_mode` configuration variable.
*/
export const Title = (props: TTitle): JSX.Element => {
export const Title = (props: FlexProps): JSX.Element => {
const { fontSize, ...rest } = props;
const { web } = useConfig();
const { titleMode } = web.text;

View file

@ -1,29 +0,0 @@
import type { FlexProps, HeadingProps, ImageProps, StackProps } from '@chakra-ui/react';
import type { MotionProps } from 'framer-motion';
export interface THeader extends FlexProps {
resetForm(): void;
}
export type THeaderLayout = {
sm: [JSX.Element, JSX.Element];
md: [JSX.Element, JSX.Element];
lg: [JSX.Element, JSX.Element];
xl: [JSX.Element, JSX.Element];
};
export type TDWrapper = Omit<StackProps, 'transition'> & MotionProps;
export type TMWrapper = Omit<StackProps, 'transition'> & MotionProps;
export interface TTitle extends FlexProps {}
export interface TTitleOnly extends HeadingProps {}
export interface TLogo extends ImageProps {}
export interface TTitleWrapper extends Partial<MotionProps & Omit<StackProps, 'transition'>> {}
export interface THeaderCtx {
showSubtitle: boolean;
titleRef: React.MutableRefObject<HTMLHeadingElement>;
}

View file

@ -1,6 +1,6 @@
export * from './animated';
export * from './card';
export * from './codeBlock';
export * from './code-block';
export * from './countdown';
export * from './custom';
export * from './debugger';
@ -8,20 +8,27 @@ export * from './directive-info-modal';
export * from './dynamic-icon';
export * from './favicon';
export * from './footer';
export * from './form';
export * from './form-field';
export * from './form-row';
export * from './greeting';
export * from './header';
export * from './label';
export * from './layout';
export * from './loadError';
export * from './load-error';
export * from './loading';
export * from './lookingGlass';
export * from './location-card';
export * from './looking-glass-form';
export * from './markdown';
export * from './meta';
export * from './output';
export * from './path';
export * from './prompt';
export * from './query-location';
export * from './query-target';
export * from './query-type';
export * from './resolved-target';
export * from './results';
export * from './select';
export * from './submit';
export * from './submit-button';
export * from './table';
export * from './user-ip';

View file

@ -3,9 +3,20 @@ import { Flex } from '@chakra-ui/react';
import { useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import type { TLabel } from './types';
import type { FlexProps } from '@chakra-ui/react';
const _Label: React.ForwardRefRenderFunction<HTMLDivElement, TLabel> = (props: TLabel, ref) => {
interface LabelProps extends FlexProps {
value: string;
label: string;
bg: string;
valueColor?: string;
labelColor?: string;
}
const _Label: React.ForwardRefRenderFunction<HTMLDivElement, LabelProps> = (
props: LabelProps,
ref,
) => {
const { value, label, labelColor, bg = 'primary.600', valueColor, ...rest } = props;
const valueColorAuto = useOpposingColor(bg);

View file

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

View file

@ -1,9 +0,0 @@
import { FlexProps } from '@chakra-ui/react';
export interface TLabel extends FlexProps {
value: string;
label: string;
bg: string;
valueColor?: string;
labelColor?: string;
}

View file

@ -6,13 +6,13 @@ import { If, Then } from 'react-if';
import { Debugger, Greeting, Footer, Header } from '~/components';
import { useConfig } from '~/context';
import { useFormState } from '~/hooks';
import { ResetButton } from './resetButton';
import { ResetButton } from './reset-button';
import type { TFrame } from './types';
import type { FlexProps } from '@chakra-ui/react';
const AnimatedFlex = motion(Flex);
export const Frame = (props: TFrame): JSX.Element => {
export const Frame = (props: FlexProps): JSX.Element => {
const { developerMode } = useConfig();
const { setStatus, reset } = useFormState(
useCallback(({ setStatus, reset }) => ({ setStatus, reset }), []),

View file

@ -1,5 +1,5 @@
import { AnimatePresence } from 'framer-motion';
import { LookingGlass, Results } from '~/components';
import { LookingGlassForm, Results } from '~/components';
import { useView } from '~/hooks';
import { Frame } from './frame';
@ -11,7 +11,7 @@ export const Layout = (): JSX.Element => {
<Results />
) : (
<AnimatePresence>
<LookingGlass />
<LookingGlassForm />
</AnimatePresence>
)}
</Frame>

View file

@ -4,9 +4,14 @@ import { AnimatedDiv, DynamicIcon } from '~/components';
import { useColorValue } from '~/context';
import { useOpposingColor, useFormState } from '~/hooks';
import type { TResetButton } from './types';
import type { FlexProps } from '@chakra-ui/react';
export const ResetButton = (props: TResetButton): JSX.Element => {
interface ResetButtonProps extends FlexProps {
developerMode: boolean;
resetForm(): void;
}
export const ResetButton = (props: ResetButtonProps): JSX.Element => {
const { developerMode, resetForm, ...rest } = props;
const status = useFormState(s => s.status);
const bg = useColorValue('primary.500', 'primary.300');

View file

@ -1,8 +0,0 @@
import type { FlexProps } from '@chakra-ui/react';
export interface TFrame extends FlexProps {}
export interface TResetButton extends FlexProps {
developerMode: boolean;
resetForm(): void;
}

View file

@ -7,7 +7,7 @@ import {
AlertTitle,
AlertDescription,
} from '@chakra-ui/react';
import { NoConfig } from './noconfig';
import { NoConfig } from './no-config';
import type { CenterProps } from '@chakra-ui/react';
import type { ConfigLoadError } from '~/util';

View file

@ -1,5 +1,5 @@
import { Spinner } from '@chakra-ui/react';
import { NoConfig } from './noconfig';
import { NoConfig } from './no-config';
export const Loading = (): JSX.Element => {
return (

View file

@ -4,8 +4,15 @@ import { motion } from 'framer-motion';
import { useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import type { LocationOption } from './queryLocation';
import type { LocationCardProps } from './types';
import type { SingleOption } from '~/types';
import type { LocationOption } from './query-location';
interface LocationCardProps {
option: SingleOption;
defaultChecked: boolean;
onChange(a: 'add' | 'remove', v: SingleOption): void;
hasError: boolean;
}
const MotionBox = motion(Box);

View file

@ -31,7 +31,7 @@ const fqdnPattern = new RegExp(
/^(?!:\/\/)([a-zA-Z0-9-]+\.)?[a-zA-Z0-9-][a-zA-Z0-9-]+\.[a-zA-Z-]{2,6}?$/im,
);
export const LookingGlass = (): JSX.Element => {
export const LookingGlassForm = (): JSX.Element => {
const { web, messages } = useConfig();
const greetingReady = useGreeting(s => s.greetingReady);

View file

@ -23,19 +23,46 @@ import { useColorValue } from '~/context';
import type {
BoxProps,
TextProps,
CodeProps,
LinkProps,
TextProps,
TableProps,
ChakraProps,
HeadingProps,
DividerProps,
TableRowProps,
TableBodyProps,
TableCellProps,
TableHeadProps,
ListProps as ChakraListProps,
HeadingProps as ChakraHeadingProps,
CheckboxProps as ChakraCheckboxProps,
TableCellProps as ChakraTableCellProps,
} from '@chakra-ui/react';
import type { TCheckbox, TList, THeading, TCodeBlock, TTableData, TListItem } from './types';
interface CheckboxProps extends ChakraCheckboxProps {
checked: boolean;
}
interface ListItemProps {
checked: boolean;
children?: React.ReactNode;
}
interface ListProps extends ChakraListProps {
ordered: boolean;
children?: React.ReactNode;
}
interface HeadingProps extends ChakraHeadingProps {
level: 1 | 2 | 3 | 4 | 5 | 6;
}
interface CodeBlockProps {
value: React.ReactNode;
}
interface TableCellProps extends BoxProps {
isHeader: boolean;
}
type MDProps = {
node: Dict;
@ -55,12 +82,12 @@ function clean<P extends ChakraProps>(props: P): P {
return props;
}
export const Checkbox = (props: TCheckbox & MDProps): JSX.Element => {
export const Checkbox = (props: CheckboxProps & MDProps): JSX.Element => {
const { checked, node, ...rest } = props;
return <ChakraCheckbox isChecked={checked} {...rest} />;
};
export const List = (props: TList): JSX.Element => {
export const List = (props: ListProps): JSX.Element => {
const { ordered, ...rest } = props;
return (
<If condition={ordered}>
@ -74,7 +101,7 @@ export const List = (props: TList): JSX.Element => {
);
};
export const ListItem = (props: TListItem & MDProps): JSX.Element => {
export const ListItem = (props: ListItemProps & MDProps): JSX.Element => {
const { checked, node, ...rest } = props;
return checked ? (
<Checkbox checked={checked} node={node} {...rest} />
@ -83,7 +110,7 @@ export const ListItem = (props: TListItem & MDProps): JSX.Element => {
);
};
export const Heading = (props: THeading): JSX.Element => {
export const Heading = (props: HeadingProps): JSX.Element => {
const { level, ...rest } = props;
const levelMap = {
@ -93,9 +120,9 @@ export const Heading = (props: THeading): JSX.Element => {
4: { as: 'h4', size: 'md', fontWeight: 'normal' },
5: { as: 'h5', size: 'md', fontWeight: 'bold' },
6: { as: 'h6', size: 'sm', fontWeight: 'bold' },
} as { [i: number]: HeadingProps };
} as { [i: number]: ChakraHeadingProps };
return <ChakraHeading {...levelMap[level]} {...clean<Omit<THeading, 'level'>>(rest)} />;
return <ChakraHeading {...levelMap[level]} {...clean<Omit<HeadingProps, 'level'>>(rest)} />;
};
export const Link = (props: LinkProps): JSX.Element => {
@ -103,7 +130,7 @@ export const Link = (props: LinkProps): JSX.Element => {
return <ChakraLink isExternal color={color} {...clean<LinkProps>(props)} />;
};
export const CodeBlock = (props: TCodeBlock): JSX.Element => (
export const CodeBlock = (props: CodeBlockProps): JSX.Element => (
<CustomCodeBlock>{props.value}</CustomCodeBlock>
);
@ -142,15 +169,15 @@ export const TableHead = (props: TableHeadProps): JSX.Element => (
<Thead {...clean<TableHeadProps>(props)} />
);
export const TableCell = (props: TTableData): JSX.Element => {
export const TableCell = (props: TableCellProps): JSX.Element => {
const { isHeader, ...rest } = props;
return (
<If condition={isHeader}>
<Then>
<Th {...clean<TableCellProps>(rest)} />
<Th {...clean<ChakraTableCellProps>(rest)} />
</Then>
<Else>
<Td {...clean<TableCellProps>(rest)} />
<Td {...clean<ChakraTableCellProps>(rest)} />
</Else>
</If>
);

View file

@ -18,7 +18,10 @@ import {
} from './elements';
import type { ReactMarkdownProps } from 'react-markdown';
import type { TMarkdown } from './types';
interface MarkdownProps {
content: string;
}
const renderers = {
break: Br,
@ -37,6 +40,6 @@ const renderers = {
thematicBreak: Divider,
} as ReactMarkdownProps['renderers'];
export const Markdown = (props: TMarkdown): JSX.Element => (
export const Markdown = (props: MarkdownProps): JSX.Element => (
<ReactMarkdown plugins={[gfm]} renderers={renderers} source={props.content} />
);

View file

@ -1,31 +0,0 @@
import type { BoxProps, CheckboxProps, HeadingProps, ListProps } from '@chakra-ui/react';
export interface TMarkdown {
content: string;
}
export interface TCheckbox extends CheckboxProps {
checked: boolean;
}
export interface TListItem {
checked: boolean;
children?: React.ReactNode;
}
export interface TList extends ListProps {
ordered: boolean;
children?: React.ReactNode;
}
export interface THeading extends HeadingProps {
level: 1 | 2 | 3 | 4 | 5 | 6;
}
export interface TCodeBlock {
value: React.ReactNode;
}
export interface TTableData extends BoxProps {
isHeader: boolean;
}

View file

@ -3,10 +3,12 @@ import { Table } from '~/components';
import { useConfig } from '~/context';
import { Cell } from './cell';
import type { TColumn, ParsedDataField, TCellRender } from '~/types';
import type { TBGPTable } from './types';
import type { FlexProps } from '@chakra-ui/react';
import type { TableColumn, ParsedDataField, CellRenderProps } from '~/types';
function makeColumns(fields: ParsedDataField[]): TColumn[] {
type BGPTableProps = Swap<FlexProps, 'children', StructuredResponse>;
function makeColumns(fields: ParsedDataField[]): TableColumn[] {
return fields.map(pair => {
const [header, accessor, align] = pair;
@ -15,7 +17,7 @@ function makeColumns(fields: ParsedDataField[]): TColumn[] {
accessor,
hidden: false,
Header: header,
} as TColumn;
} as TableColumn;
if (align === null) {
columnConfig.hidden = true;
@ -25,7 +27,7 @@ function makeColumns(fields: ParsedDataField[]): TColumn[] {
});
}
export const BGPTable = (props: TBGPTable): JSX.Element => {
export const BGPTable = (props: BGPTableProps): JSX.Element => {
const { children: data, ...rest } = props;
const { parsedDataFields } = useConfig();
const columns = makeColumns(parsedDataFields);
@ -38,7 +40,7 @@ export const BGPTable = (props: TBGPTable): JSX.Element => {
data={data.routes}
rowHighlightBg="green"
rowHighlightProp="active"
Cell={(d: TCellRender) => <Cell data={d} rawData={data} />}
Cell={(d: CellRenderProps) => <Cell data={d} rawData={data} />}
/>
</Flex>
);

View file

@ -1,8 +1,13 @@
import { MonoField, Active, Weight, Age, Communities, RPKIState, ASPath } from './fields';
import type { TCell } from './types';
import type { CellRenderProps } from '~/types';
export const Cell = (props: TCell): JSX.Element => {
interface CellProps {
data: CellRenderProps;
rawData: StructuredResponse;
}
export const Cell = (props: CellProps): JSX.Element => {
const { data, rawData } = props;
const cellId = data.column.id as keyof Route;
const component = {

View file

@ -8,20 +8,47 @@ import { DynamicIcon } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import type {
TAge,
TActive,
TWeight,
TASPath,
TMonoField,
TRPKIState,
TCommunities,
} from './types';
import type { TextProps } from '@chakra-ui/react';
interface ActiveProps {
isActive: boolean;
}
interface MonoFieldProps extends TextProps {
v: React.ReactNode;
}
interface AgeProps extends TextProps {
inSeconds: number;
}
interface WeightProps extends TextProps {
weight: number;
winningWeight: 'low' | 'high';
}
interface ASPathProps {
path: number[];
active: boolean;
}
interface CommunitiesProps {
communities: string[];
}
interface RPKIStateProps {
state:
| 0 // Invalid
| 1 // Valid
| 2 // Unknown
| 3; // Unverified
active: boolean;
}
dayjs.extend(relativeTimePlugin);
dayjs.extend(utcPlugin);
export const MonoField = (props: TMonoField): JSX.Element => {
export const MonoField = (props: MonoFieldProps): JSX.Element => {
const { v, ...rest } = props;
return (
<Text as="span" fontSize="sm" fontFamily="mono" {...rest}>
@ -30,7 +57,7 @@ export const MonoField = (props: TMonoField): JSX.Element => {
);
};
export const Active = (props: TActive): JSX.Element => {
export const Active = (props: ActiveProps): JSX.Element => {
const { isActive } = props;
const color = useColorValue(['gray.500', 'green.500'], ['whiteAlpha.300', 'blackAlpha.500']);
return (
@ -45,7 +72,7 @@ export const Active = (props: TActive): JSX.Element => {
);
};
export const Age = (props: TAge): JSX.Element => {
export const Age = (props: AgeProps): JSX.Element => {
const { inSeconds, ...rest } = props;
const now = dayjs.utc();
const then = now.subtract(inSeconds, 'second');
@ -58,7 +85,7 @@ export const Age = (props: TAge): JSX.Element => {
);
};
export const Weight = (props: TWeight): JSX.Element => {
export const Weight = (props: WeightProps): JSX.Element => {
const { weight, winningWeight, ...rest } = props;
const fixMeText =
winningWeight === 'low' ? 'Lower Weight is Preferred' : 'Higher Weight is Preferred';
@ -71,7 +98,7 @@ export const Weight = (props: TWeight): JSX.Element => {
);
};
export const ASPath = (props: TASPath): JSX.Element => {
export const ASPath = (props: ASPathProps): JSX.Element => {
const { path, active } = props;
const color = useColorValue(
// light: inactive, active
@ -108,7 +135,7 @@ export const ASPath = (props: TASPath): JSX.Element => {
return <>{paths}</>;
};
export const Communities = (props: TCommunities): JSX.Element => {
export const Communities = (props: CommunitiesProps): JSX.Element => {
const { communities } = props;
const { web } = useConfig();
const bg = useColorValue('white', 'gray.900');
@ -147,8 +174,8 @@ export const Communities = (props: TCommunities): JSX.Element => {
);
};
const _RPKIState: React.ForwardRefRenderFunction<HTMLDivElement, TRPKIState> = (
props: TRPKIState,
const _RPKIState: React.ForwardRefRenderFunction<HTMLDivElement, RPKIStateProps> = (
props: RPKIStateProps,
ref,
) => {
const { state, active } = props;
@ -194,4 +221,4 @@ const _RPKIState: React.ForwardRefRenderFunction<HTMLDivElement, TRPKIState> = (
);
};
export const RPKIState = forwardRef<HTMLDivElement, TRPKIState>(_RPKIState);
export const RPKIState = forwardRef<HTMLDivElement, RPKIStateProps>(_RPKIState);

View file

@ -1,2 +1,2 @@
export * from './table';
export * from './text';
export * from './bgp-table';
export * from './text-output';

View file

@ -2,9 +2,11 @@ import { Box } from '@chakra-ui/react';
import { useColorValue, useConfig } from '~/context';
import { Highlighted } from './highlighted';
import type { TTextOutput } from './types';
import type { BoxProps } from '@chakra-ui/react';
export const TextOutput = (props: TTextOutput): JSX.Element => {
type TextOutputProps = Swap<BoxProps, 'children', string>;
export const TextOutput = (props: TextOutputProps): JSX.Element => {
const { children, ...rest } = props;
const bg = useColorValue('blackAlpha.100', 'gray.800');

View file

@ -1,50 +0,0 @@
import type { BoxProps, FlexProps, TextProps } from '@chakra-ui/react';
import type { TCellRender } from '~/types';
export interface TTextOutput extends Omit<BoxProps, 'children'> {
children: string;
}
export interface TActive {
isActive: boolean;
}
export interface TMonoField extends TextProps {
v: React.ReactNode;
}
export interface TAge extends TextProps {
inSeconds: number;
}
export interface TWeight extends TextProps {
weight: number;
winningWeight: 'low' | 'high';
}
export interface TASPath {
path: number[];
active: boolean;
}
export interface TCommunities {
communities: string[];
}
export interface TRPKIState {
state:
| 0 // Invalid
| 1 // Valid
| 2 // Unknown
| 3; // Unverified
active: boolean;
}
export interface TCell {
data: TCellRender;
rawData: StructuredResponse;
}
export interface TBGPTable extends Omit<FlexProps, 'children'> {
children: StructuredResponse;
}

View file

@ -5,9 +5,24 @@ import { useASNDetail } from '~/hooks';
import { Controls } from './controls';
import { useElements } from './useElements';
import type { TChart, TNode, TNodeData } from './types';
import type { NodeProps as ReactFlowNodeProps } from 'react-flow-renderer';
export const Chart = (props: TChart): JSX.Element => {
interface ChartProps {
data: StructuredResponse;
}
interface NodeProps<D extends unknown> extends Omit<ReactFlowNodeProps, 'data'> {
data: D;
}
interface NodeData {
asn: string;
name: string;
hasChildren: boolean;
hasParents?: boolean;
}
export const Chart = (props: ChartProps): JSX.Element => {
const { data } = props;
const { primaryAsn, orgName } = useConfig();
@ -32,7 +47,7 @@ export const Chart = (props: TChart): JSX.Element => {
);
};
const ASNode = (props: TNode<TNodeData>): JSX.Element => {
const ASNode = (props: NodeProps<NodeData>): JSX.Element => {
const { data } = props;
const { asn, name, hasChildren, hasParents } = data;

View file

@ -1,9 +1,11 @@
import { Button, Tooltip } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import type { TPathButton } from './types';
interface PathButtonProps {
onOpen(): void;
}
export const PathButton = (props: TPathButton): JSX.Element => {
export const PathButton = (props: PathButtonProps): JSX.Element => {
const { onOpen } = props;
return (
<Tooltip hasArrow label="View AS Path" placement="top">

View file

@ -10,12 +10,14 @@ import {
} from '@chakra-ui/react';
import { useColorValue, useBreakpointValue } from '~/context';
import { useFormState } from '~/hooks';
import { PathButton } from './button';
import { PathButton } from './path-button';
import { Chart } from './chart';
import type { TPath } from './types';
interface PathProps {
device: string;
}
export const Path = (props: TPath): JSX.Element => {
export const Path = (props: PathProps): JSX.Element => {
const { device } = props;
const displayTarget = useFormState(s => s.target.display);
const getResponse = useFormState(s => s.response);

View file

@ -1,29 +0,0 @@
import type { NodeProps } from 'react-flow-renderer';
export interface TChart {
data: StructuredResponse;
}
export interface TPath {
device: string;
}
export interface TNode<D extends unknown> extends Omit<NodeProps, 'data'> {
data: D;
}
export interface TNodeData {
asn: string;
name: string;
hasChildren: boolean;
hasParents?: boolean;
}
export interface BasePath {
asn: string;
name: string;
}
export interface TPathButton {
onOpen(): void;
}

View file

@ -3,7 +3,11 @@ import { useMemo } from 'react';
import isEqual from 'react-fast-compare';
import type { FlowElement } from 'react-flow-renderer';
import type { BasePath } from './types';
interface BasePath {
asn: string;
name: string;
}
const NODE_WIDTH = 200;
const NODE_HEIGHT = 48;

View file

@ -1,19 +1,22 @@
import { useMemo } from 'react';
import { Wrap, VStack, Flex, chakra } from '@chakra-ui/react';
import { useFormContext } from 'react-hook-form';
import { Select } from '~/components';
import { Select, LocationCard } from '~/components';
import { useConfig } from '~/context';
import { useFormState } from '~/hooks';
import { isMultiValue, isSingleValue } from '~/components/select';
import { LocationCard } from './location-card';
import type { DeviceGroup, SingleOption, OptionGroup, FormData } from '~/types';
import type { DeviceGroup, SingleOption, OptionGroup, FormData, OnChangeArgs } from '~/types';
import type { SelectOnChange } from '~/components/select';
import type { TQuerySelectField } from './types';
/** Location option type alias for future extensions. */
export type LocationOption = SingleOption;
interface QueryLocationProps {
onChange: (f: OnChangeArgs) => void;
label: string;
}
function buildOptions(devices: DeviceGroup[]): OptionGroup<LocationOption>[] {
return devices
.map(group => {
@ -37,7 +40,7 @@ function buildOptions(devices: DeviceGroup[]): OptionGroup<LocationOption>[] {
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
}
export const QueryLocation = (props: TQuerySelectField): JSX.Element => {
export const QueryLocation = (props: QueryLocationProps): JSX.Element => {
const { onChange, label } = props;
const {

View file

@ -7,15 +7,22 @@ import { isSingleValue } from '~/components/select';
import { useColorValue } from '~/context';
import { useDirective, useFormState } from '~/hooks';
import { isSelectDirective } from '~/types';
import { UserIP } from './userIP';
import { UserIP } from './user-ip';
import type { OptionProps, GroupBase } from 'react-select';
import type { UseFormRegister } from 'react-hook-form';
import type { SelectOnChange } from '~/components/select';
import type { Directive, SingleOption } from '~/types';
import type { TQueryTarget } from './types';
import type { Directive, SingleOption, OnChangeArgs, FormData } from '~/types';
type OptionWithDescription = SingleOption<{ description: string | null }>;
interface QueryTargetProps {
name: string;
placeholder: string;
onChange(e: OnChangeArgs): void;
register: UseFormRegister<FormData>;
}
function buildOptions(directive: Nullable<Directive>): OptionWithDescription[] {
if (directive !== null && isSelectDirective(directive)) {
return directive.options.map(o => ({
@ -40,7 +47,7 @@ const Option = (props: OptionProps<OptionWithDescription, false>) => {
);
};
export const QueryTarget = (props: TQueryTarget): JSX.Element => {
export const QueryTarget = (props: QueryTargetProps): JSX.Element => {
const { name, register, onChange, placeholder } = props;
const bg = useColorValue('white', 'blackSolid.800');

View file

@ -9,14 +9,14 @@ import { isSingleValue } from '~/components/select';
import type { UseRadioProps } from '@chakra-ui/react';
import type { MenuListProps } from 'react-select';
import type { SingleOption, OptionGroup, OptionsOrGroup } from '~/types';
import type { SingleOption, OptionGroup, OptionsOrGroup, OnChangeArgs } from '~/types';
import type { SelectOnChange } from '~/components/select';
import type { TQuerySelectField } from './types';
type QueryTypeOption = SingleOption<{ group?: string }>;
function sorter<T extends QueryTypeOption | OptionGroup<QueryTypeOption>>(a: T, b: T): number {
return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
interface QueryTypeProps {
onChange: (f: OnChangeArgs) => void;
label: string;
}
type UserFilter = {
@ -25,6 +25,10 @@ type UserFilter = {
filter(candidate: QueryTypeOption, input: string): boolean;
};
function sorter<T extends QueryTypeOption | OptionGroup<QueryTypeOption>>(a: T, b: T): number {
return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
}
const useFilter = create<UserFilter>((set, get) => ({
selected: '',
setSelected(newValue: string) {
@ -146,7 +150,7 @@ const MenuList = (props: MenuListProps<QueryTypeOption, boolean>): JSX.Element =
);
};
export const QueryType = (props: TQuerySelectField): JSX.Element => {
export const QueryType = (props: QueryTypeProps): JSX.Element => {
const { onChange, label } = props;
const {
formState: { errors },

View file

@ -5,7 +5,10 @@ import { useConfig, useColorValue } from '~/context';
import { useStrf, useDNSQuery, useFormState } from '~/hooks';
import type { DnsOverHttps } from '~/types';
import type { ResolvedTargetProps } from './types';
interface ResolvedTargetProps {
errorClose(): void;
}
function findAnswer(data: DnsOverHttps.Response | undefined): string {
let answer = '';

View file

@ -1,9 +1,13 @@
import { Button, Tooltip, useClipboard } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import type { TCopyButton } from './types';
import type { ButtonProps } from '@chakra-ui/react';
export const CopyButton = (props: TCopyButton): JSX.Element => {
interface CopyButtonProps extends ButtonProps {
copyValue: string;
}
export const CopyButton = (props: CopyButtonProps): JSX.Element => {
const { copyValue, ...rest } = props;
const { onCopy, hasCopied } = useClipboard(copyValue);
return (

View file

@ -1,10 +1,13 @@
import { Text } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import type { TFormattedError } from './types';
interface FormattedErrorProps {
keywords: string[];
message: string;
}
type TFormatError = string | JSX.Element;
type FormatError = string | JSX.Element;
function formatError(text: string, values: string[], regex: RegExp): TFormatError[] | TFormatError {
function formatError(text: string, values: string[], regex: RegExp): FormatError[] | FormatError {
if (!values.length) {
return text;
}
@ -19,16 +22,16 @@ function formatError(text: string, values: string[], regex: RegExp): TFormatErro
return prev.concat(
values.includes(current) ? <strong key={i + current}>{current}</strong> : current,
);
}, [] as TFormatError[]);
}, [] as FormatError[]);
}
export const FormattedError = (props: TFormattedError): JSX.Element => {
export const FormattedError = (props: FormattedErrorProps): JSX.Element => {
const { keywords, message } = props;
const pattern = new RegExp(keywords.map(kw => `(${kw})`).join('|'), 'gi');
const things = formatError(message, keywords, pattern);
return (
<Text as="span" fontWeight={keywords.length === 0 ? 'bold' : undefined}>
<chakra.span fontWeight={keywords.length === 0 ? 'bold' : undefined}>
{keywords.length !== 0 ? things : message}
</Text>
</chakra.span>
);
};

View file

@ -4,7 +4,16 @@ import { DynamicIcon } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { useOpposingColor, useStrf } from '~/hooks';
import type { TResultHeader } from './types';
import type { ErrorLevels } from '~/types';
interface ResultHeaderProps {
title: string;
loading: boolean;
isError?: boolean;
errorMsg: string;
errorLevel: ErrorLevels;
runtime: number;
}
const runtimeText = (runtime: number, text: string): string => {
let unit = 'seconds';
@ -14,7 +23,7 @@ const runtimeText = (runtime: number, text: string): string => {
return `${text} ${unit}`;
};
export const ResultHeader = (props: TResultHeader): JSX.Element => {
export const ResultHeader = (props: ResultHeaderProps): JSX.Element => {
const { title, loading, isError, errorMsg, errorLevel, runtime } = props;
const status = useColorValue('primary.500', 'primary.300');

View file

@ -21,12 +21,17 @@ import { useColorValue, useConfig, useMobile } from '~/context';
import { useStrf, useLGQuery, useTableToString, useFormState, useDevice } from '~/hooks';
import { isStructuredOutput, isStringOutput } from '~/types';
import { isStackError, isFetchError, isLGError, isLGOutputOrError } from './guards';
import { RequeryButton } from './requeryButton';
import { CopyButton } from './copyButton';
import { FormattedError } from './error';
import { RequeryButton } from './requery-button';
import { CopyButton } from './copy-button';
import { FormattedError } from './formatted-error';
import { ResultHeader } from './header';
import type { ResultProps, TErrorLevels } from './types';
import type { ErrorLevels } from '~/types';
interface ResultProps {
index: number;
queryLocation: string;
}
const AnimatedAccordionItem = motion(AccordionItem);
@ -106,7 +111,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
}
}, [error, data, messages.general, messages.requestTimeout]);
const errorLevel = useMemo<TErrorLevels>(() => {
const errorLevel = useMemo<ErrorLevels>(() => {
const statusMap = {
success: 'success',
warning: 'warning',
@ -114,7 +119,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
danger: 'error',
} as { [K in ResponseLevel]: 'success' | 'warning' | 'error' };
let e: TErrorLevels = 'error';
let e: ErrorLevels = 'error';
if (isLGError(error)) {
const idx = error.level as ResponseLevel;
@ -182,103 +187,101 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
'&:last-of-type': { borderBottom: 'none' },
}}
>
<>
<AccordionHeaderWrapper>
<AccordionButton py={2} w="unset" _hover={{}} _focus={{}} flex="1 0 auto">
<ResultHeader
isError={isError}
loading={isLoading}
errorMsg={errorMsg}
errorLevel={errorLevel}
runtime={data?.runtime ?? 0}
title={device.name}
/>
</AccordionButton>
<HStack py={2} spacing={1}>
{isStructuredOutput(data) && data.level === 'success' && tableComponent && (
<Path device={device.id} />
<AccordionHeaderWrapper>
<AccordionButton py={2} w="unset" _hover={{}} _focus={{}} flex="1 0 auto">
<ResultHeader
isError={isError}
loading={isLoading}
errorMsg={errorMsg}
errorLevel={errorLevel}
runtime={data?.runtime ?? 0}
title={device.name}
/>
</AccordionButton>
<HStack py={2} spacing={1}>
{isStructuredOutput(data) && data.level === 'success' && tableComponent && (
<Path device={device.id} />
)}
<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} variant="solid">
<FormattedError message={data.output} keywords={errorKeywords} />
</Alert>
) : (
<Alert rounded="lg" my={2} py={4} status={errorLevel} variant="solid">
<FormattedError message={errorMsg} keywords={errorKeywords} />
</Alert>
)}
</>
) : (
<Alert rounded="lg" my={2} py={4} status={errorLevel} variant="solid">
<FormattedError message={errorMsg} keywords={errorKeywords} />
</Alert>
)}
<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} variant="solid">
<FormattedError message={data.output} keywords={errorKeywords} />
</Alert>
) : (
<Alert rounded="lg" my={2} py={4} status={errorLevel} variant="solid">
<FormattedError message={errorMsg} keywords={errorKeywords} />
</Alert>
)}
</>
) : (
<Alert rounded="lg" my={2} py={4} status={errorLevel} variant="solid">
<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 condition={cache.showText && !isError && isCached}>
<If condition={isMobile}>
<Then>
<Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
<Tooltip hasArrow label={cacheLabel} placement="top">
<Box>
<DynamicIcon icon={{ bs: 'BsLightningFill' }} color={color} />
</Box>
</Tooltip>
</Then>
<Else>
<Tooltip hasArrow label={cacheLabel} placement="top">
<Box>
<DynamicIcon icon={{ bs: 'BsLightningFill' }} color={color} />
</Box>
</Tooltip>
<Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
</Else>
</If>
</If>
</HStack>
</Flex>
</AccordionPanel>
</>
</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 condition={cache.showText && !isError && isCached}>
<If condition={isMobile}>
<Then>
<Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
<Tooltip hasArrow label={cacheLabel} placement="top">
<Box>
<DynamicIcon icon={{ bs: 'BsLightningFill' }} color={color} />
</Box>
</Tooltip>
</Then>
<Else>
<Tooltip hasArrow label={cacheLabel} placement="top">
<Box>
<DynamicIcon icon={{ bs: 'BsLightningFill' }} color={color} />
</Box>
</Tooltip>
<Countdown timeout={cache.timeout} text={web.text.cachePrefix} />
</Else>
</If>
</If>
</HStack>
</Flex>
</AccordionPanel>
</AnimatedAccordionItem>
);
};

View file

@ -2,10 +2,15 @@ import { forwardRef } from 'react';
import { Button, Tooltip } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import type { TRequeryButton } from './types';
import type { ButtonProps } from '@chakra-ui/react';
import type { UseQueryResult } from 'react-query';
const _RequeryButton: React.ForwardRefRenderFunction<HTMLButtonElement, TRequeryButton> = (
props: TRequeryButton,
interface RequeryButtonProps extends ButtonProps {
requery: Get<UseQueryResult<QueryResponse>, 'refetch'>;
}
const _RequeryButton: React.ForwardRefRenderFunction<HTMLButtonElement, RequeryButtonProps> = (
props: RequeryButtonProps,
ref,
) => {
const { requery, ...rest } = props;
@ -19,7 +24,7 @@ const _RequeryButton: React.ForwardRefRenderFunction<HTMLButtonElement, TRequery
size="sm"
zIndex="1"
variant="ghost"
onClick={requery as TRequeryButton['onClick']}
onClick={requery as Get<RequeryButtonProps, 'onClick'>}
colorScheme="secondary"
{...rest}
>
@ -29,4 +34,4 @@ const _RequeryButton: React.ForwardRefRenderFunction<HTMLButtonElement, TRequery
);
};
export const RequeryButton = forwardRef<HTMLButtonElement, TRequeryButton>(_RequeryButton);
export const RequeryButton = forwardRef<HTMLButtonElement, RequeryButtonProps>(_RequeryButton);

View file

@ -1,31 +0,0 @@
import type { ButtonProps } from '@chakra-ui/react';
import type { UseQueryResult } from 'react-query';
export interface TResultHeader {
title: string;
loading: boolean;
isError?: boolean;
errorMsg: string;
errorLevel: 'success' | 'warning' | 'error';
runtime: number;
}
export interface TFormattedError {
keywords: string[];
message: string;
}
export interface ResultProps {
index: number;
queryLocation: string;
}
export type TErrorLevels = 'success' | 'warning' | 'error';
export interface TCopyButton extends ButtonProps {
copyValue: string;
}
export interface TRequeryButton extends ButtonProps {
requery: UseQueryResult<QueryResponse>['refetch'];
}

View file

@ -26,14 +26,14 @@ import type {
SelectInstance,
} from 'react-select';
import type { SingleOption } from '~/types';
import type { TSelectBase, TSelectContext } from './types';
import type { SelectProps, SelectContextProps } from './types';
const SelectContext = createContext<TSelectContext>({} as TSelectContext);
export const useSelectContext = (): TSelectContext => useContext(SelectContext);
const SelectContext = createContext<SelectContextProps>({} as SelectContextProps);
export const useSelectContext = (): SelectContextProps => useContext(SelectContext);
export const Select = forwardRef(
<Opt extends SingleOption = SingleOption, IsMulti extends boolean = boolean>(
props: TSelectBase<Opt, IsMulti>,
props: SelectProps<Opt, IsMulti>,
ref: React.Ref<SelectInstance<Opt, IsMulti>>,
): JSX.Element => {
const { options, isMulti, onSelect, isError = false, components, ...rest } = props;

View file

@ -6,9 +6,9 @@ import type { Theme, SingleOption } from '~/types';
export type SelectOnChange<
Opt extends SingleOption = SingleOption,
IsMulti extends boolean = boolean,
> = NonNullable<ReactSelect.Props<Opt, IsMulti>['onChange']>;
> = NonNullable<Get<ReactSelect.Props<Opt, IsMulti>, 'onChange'>>;
export interface TSelectBase<Opt extends SingleOption, IsMulti extends boolean>
export interface SelectProps<Opt extends SingleOption, IsMulti extends boolean>
extends ReactSelect.Props<Opt, IsMulti> {
name: string;
isMulti?: IsMulti;
@ -18,7 +18,7 @@ export interface TSelectBase<Opt extends SingleOption, IsMulti extends boolean>
colorScheme?: Theme.ColorNames;
}
export interface TSelectContext {
export interface SelectContextProps {
colorMode: 'light' | 'dark';
isOpen: boolean;
isError: boolean;

View file

@ -20,7 +20,14 @@ import { useMobile, useColorValue } from '~/context';
import { useFormState } from '~/hooks';
import type { IconButtonProps } from '@chakra-ui/react';
import type { SubmitButtonProps, ResponsiveSubmitButtonProps } from './types';
type SubmitButtonProps = Omit<IconButtonProps, 'aria-label'>;
interface ResponsiveSubmitButtonProps {
isOpen: boolean;
onClose(): void;
children: React.ReactNode;
}
const _SubmitIcon: React.ForwardRefRenderFunction<
HTMLButtonElement,

View file

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

View file

@ -1,9 +0,0 @@
import type { IconButtonProps } from '@chakra-ui/react';
export type SubmitButtonProps = Omit<IconButtonProps, 'aria-label'>;
export interface ResponsiveSubmitButtonProps {
isOpen: boolean;
onClose(): void;
children: React.ReactNode;
}

View file

@ -1,6 +1,8 @@
import { IconButton } from '@chakra-ui/react';
import type { TTableIconButton } from './types';
import type { IconButtonProps } from '@chakra-ui/react';
type TTableIconButton = Omit<IconButtonProps, 'aria-label'>;
export const TableIconButton = (props: TTableIconButton): JSX.Element => (
<IconButton size="sm" borderWidth={1} {...props} aria-label="Table Icon Button" />

View file

@ -1,9 +1,14 @@
import { chakra } from '@chakra-ui/react';
import { useColorValue } from '~/context';
import type { TTableCell } from './types';
import type { BoxProps } from '@chakra-ui/react';
export const TableCell = (props: TTableCell): JSX.Element => {
interface TableCellProps extends Omit<BoxProps, 'align'> {
bordersVertical?: [boolean, number];
align?: 'left' | 'right' | 'center';
}
export const TableCell = (props: TableCellProps): JSX.Element => {
const { bordersVertical = [false, 0], align, ...rest } = props;
const [doVerticalBorders, index] = bordersVertical;
const borderLeftColor = useColorValue('blackAlpha.100', 'whiteAlpha.100');

View file

@ -1,7 +1 @@
export * from './body';
export * from './button';
export * from './cell';
export * from './head';
export * from './main';
export * from './pageSelect';
export * from './row';

View file

@ -11,13 +11,24 @@ import { TableHead } from './head';
import { TableRow } from './row';
import { TableBody } from './body';
import { TableIconButton } from './button';
import { TableSelectShow } from './pageSelect';
import { PageSelect } from './page-select';
import type { TableOptions, PluginHook } from 'react-table';
import type { TCellRender } from '~/types';
import type { TTable } from './types';
import type { Theme, TableColumn, CellRenderProps } from '~/types';
export const Table = (props: TTable): JSX.Element => {
interface TableProps {
data: Route[];
striped?: boolean;
columns: TableColumn[];
heading?: React.ReactNode;
bordersVertical?: boolean;
bordersHorizontal?: boolean;
Cell?: React.FC<CellRenderProps>;
rowHighlightProp?: keyof Route;
rowHighlightBg?: Theme.ColorNames;
}
export const Table = (props: TableProps): JSX.Element => {
const {
data,
columns,
@ -121,7 +132,7 @@ export const Table = (props: TTable): JSX.Element => {
{...row.getRowProps()}
>
{row.cells.map((cell, i) => {
const { column, row, value } = cell as TCellRender;
const { column, row, value } = cell as CellRenderProps;
return (
<TableCell
align={cell.column.align}
@ -164,7 +175,7 @@ export const Table = (props: TTable): JSX.Element => {
</strong>{' '}
</Text>
{!isMobile && (
<TableSelectShow
<PageSelect
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));

View file

@ -2,7 +2,7 @@ import { Select } from '@chakra-ui/react';
import type { SelectProps } from '@chakra-ui/react';
export const TableSelectShow = (props: SelectProps): JSX.Element => {
export const PageSelect = (props: SelectProps): JSX.Element => {
const { value, ...rest } = props;
return (
<Select size="sm" {...rest}>

View file

@ -2,9 +2,19 @@ import { chakra } from '@chakra-ui/react';
import { useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import type { TTableRow } from './types';
import type { BoxProps } from '@chakra-ui/react';
export const TableRow = (props: TTableRow): JSX.Element => {
import type { Theme } from '~/types';
interface TableRowProps extends BoxProps {
highlightBg?: Theme.ColorNames;
doHorizontalBorders?: boolean;
highlight?: boolean;
doStripe?: boolean;
index: number;
}
export const TableRow = (props: TableRowProps): JSX.Element => {
const {
index = 0,
doStripe = false,

View file

@ -1,30 +0,0 @@
import type { BoxProps, IconButtonProps } from '@chakra-ui/react';
import type { Theme, TColumn, TCellRender } from '~/types';
export interface TTable {
data: Route[];
striped?: boolean;
columns: TColumn[];
heading?: React.ReactNode;
bordersVertical?: boolean;
bordersHorizontal?: boolean;
Cell?: React.FC<TCellRender>;
rowHighlightProp?: keyof Route;
rowHighlightBg?: Theme.ColorNames;
}
export interface TTableCell extends Omit<BoxProps, 'align'> {
bordersVertical?: [boolean, number];
align?: 'left' | 'right' | 'center';
}
export interface TTableRow extends BoxProps {
highlightBg?: Theme.ColorNames;
doHorizontalBorders?: boolean;
highlight?: boolean;
doStripe?: boolean;
index: number;
}
export type TTableIconButton = Omit<IconButtonProps, 'aria-label'>;

View file

@ -4,7 +4,9 @@ import { DynamicIcon, Prompt } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { useStrf, useWtf } from '~/hooks';
import type { UserIPProps } from './types';
interface UserIPProps {
setTarget(target: string): void;
}
export const UserIP = (props: UserIPProps): JSX.Element => {
const { setTarget } = props;

View file

@ -1,48 +1,4 @@
import type { UseQueryOptions } from 'react-query';
import type * as ReactGA from 'react-ga';
import type { Device, TFormQuery } from '~/types';
export type LGQueryKey = [string, TFormQuery];
export type DNSQueryKey = [string, { target: string | null; family: 4 | 6 }];
export type LGQueryOptions = Omit<
UseQueryOptions<QueryResponse, Response | QueryResponse | Error, QueryResponse, LGQueryKey>,
| 'queryKey'
| 'queryFn'
| 'cacheTime'
| 'refetchOnWindowFocus'
| 'refetchInterval'
| 'refetchOnMount'
>;
export interface TOpposingOptions {
light?: string;
dark?: string;
}
export type UseDevice = (
/**
* Device's ID, e.g. the device.name field.
*/
deviceId: string,
) => Device | null;
export type UseStrfArgs = { [k: string]: unknown } | string;
export type TTableToStringFormatter =
| ((v: string) => string)
| ((v: number) => string)
| ((v: number[]) => string)
| ((v: string[]) => string)
| ((v: boolean) => string);
export type TTableToStringFormatted = {
age: (v: number) => string;
active: (v: boolean) => string;
as_path: (v: number[]) => string;
communities: (v: string[]) => string;
rpki_state: (v: number, n: RPKIState) => string;
};
export type GAEffect = (ga: typeof ReactGA) => void;

View file

@ -1,9 +1,18 @@
import { useQuery } from 'react-query';
import type { QueryFunctionContext, QueryObserverResult, QueryFunction } from 'react-query';
import type { TASNQuery } from '~/types';
const query: QueryFunction<TASNQuery, string> = async (ctx: QueryFunctionContext) => {
interface ASNQuery {
data: {
asn: {
organization: {
orgName: string;
} | null;
};
};
}
const query: QueryFunction<ASNQuery, string> = async (ctx: QueryFunctionContext) => {
const asn = ctx.queryKey;
const res = await fetch('https://api.asrank.caida.org/v2/graphql', {
mode: 'cors',
@ -19,8 +28,8 @@ const query: QueryFunction<TASNQuery, string> = async (ctx: QueryFunctionContext
* Query the Caida AS Rank API to get an ASN's organization name for the AS Path component.
* @see https://api.asrank.caida.org/v2/docs
*/
export function useASNDetail(asn: string): QueryObserverResult<TASNQuery> {
return useQuery<TASNQuery, unknown, TASNQuery, string>({
export function useASNDetail(asn: string): QueryObserverResult<ASNQuery> {
return useQuery<ASNQuery, unknown, ASNQuery, string>({
queryKey: asn,
queryFn: query,
refetchOnWindowFocus: false,

View file

@ -4,7 +4,8 @@ import { fetchWithTimeout } from '~/util';
import type { QueryFunction, QueryFunctionContext, QueryObserverResult } from 'react-query';
import type { DnsOverHttps } from '~/types';
import type { DNSQueryKey } from './types';
type DNSQueryKey = [string, { target: string | null; family: 4 | 6 }];
/**
* Perform a DNS over HTTPS query using the application/dns-json MIME type.

View file

@ -2,19 +2,26 @@ import { useCallback, useMemo } from 'react';
import { useConfig } from '~/context';
import type { Device } from '~/types';
import type { UseDevice } from './types';
export type UseDeviceReturn = (
/** Device's ID, e.g. the device.name field.*/
deviceId: string,
) => Nullable<Device>;
/**
* Get a device's configuration from the global configuration context based on its name.
*/
export function useDevice(): UseDevice {
export function useDevice(): UseDeviceReturn {
const { devices } = useConfig();
const locations = useMemo(() => devices.map(group => group.locations).flat(), [devices]);
const locations = useMemo<Device[]>(
() => devices.map(group => group.locations).flat(),
[devices],
);
function getDevice(id: string): Device | null {
function getDevice(id: string): Nullable<Device> {
return locations.find(device => device.id === id) ?? null;
}
return useCallback(getDevice, [locations]);
return useCallback<UseDeviceReturn>(getDevice, [locations]);
}

View file

@ -6,7 +6,7 @@ import type { Directive } from '~/types';
export function useDirective(): Nullable<Directive> {
const { getDirective, form } = useFormState(({ getDirective, form }) => ({ getDirective, form }));
return useMemo((): Nullable<Directive> => {
return useMemo<Nullable<Directive>>(() => {
if (form.queryType === '') {
return null;
}

View file

@ -9,7 +9,7 @@ import type { StateCreator } from 'zustand';
import type { UseFormSetError, UseFormClearErrors } from 'react-hook-form';
import type { SingleValue, MultiValue } from 'react-select';
import type { SingleOption, Directive, FormData, Text, Device } from '~/types';
import type { UseDevice } from './types';
import type { UseDeviceReturn } from './useDevice';
type FormStatus = 'form' | 'results';
@ -74,7 +74,7 @@ interface FormStateType<Opt extends SingleOption = SingleOption> {
extra: {
setError: UseFormSetError<FormData>;
clearErrors: UseFormClearErrors<FormData>;
getDevice: UseDevice;
getDevice: UseDeviceReturn;
text: Text;
},
): void;
@ -139,7 +139,7 @@ const formState: StateCreator<FormStateType> = (set, get) => ({
extra: {
setError: UseFormSetError<FormData>;
clearErrors: UseFormClearErrors<FormData>;
getDevice: UseDevice;
getDevice: UseDeviceReturn;
text: Text;
},
): void {

View file

@ -3,15 +3,31 @@ import { useQuery } from 'react-query';
import { useConfig } from '~/context';
import { fetchWithTimeout } from '~/util';
import type { QueryFunction, QueryFunctionContext, QueryObserverResult } from 'react-query';
import type { TFormQuery } from '~/types';
import type { LGQueryKey, LGQueryOptions } from './types';
import type {
QueryFunction,
UseQueryOptions,
QueryObserverResult,
QueryFunctionContext,
} from 'react-query';
import type { FormQuery } from '~/types';
type LGQueryKey = [string, FormQuery];
type LGQueryOptions = Omit<
UseQueryOptions<QueryResponse, Response | QueryResponse | Error, QueryResponse, LGQueryKey>,
| 'queryKey'
| 'queryFn'
| 'cacheTime'
| 'refetchOnWindowFocus'
| 'refetchInterval'
| 'refetchOnMount'
>;
/**
* Custom hook handle submission of a query to the hyperglass backend.
*/
export function useLGQuery(
query: TFormQuery,
query: FormQuery,
options: LGQueryOptions = {} as LGQueryOptions,
): QueryObserverResult<QueryResponse> {
const { requestTimeout, cache } = useConfig();

View file

@ -2,7 +2,10 @@ import { useMemo, useCallback } from 'react';
import { getColor, isLight } from '@chakra-ui/theme-tools';
import { useTheme } from '~/context';
import type { TOpposingOptions } from './types';
interface OpposingColorOptions {
light?: string;
dark?: string;
}
export type UseIsDarkCallbackReturn = (color: string) => boolean;
@ -37,7 +40,7 @@ export function useIsDarkCallback(): UseIsDarkCallbackReturn {
/**
* Determine if the foreground color for `color` should be white or black.
*/
export function useOpposingColor(color: string, options?: TOpposingOptions): string {
export function useOpposingColor(color: string, options?: OpposingColorOptions): string {
const isBlack = useIsDark(color);
return useMemo(() => {
@ -49,7 +52,9 @@ export function useOpposingColor(color: string, options?: TOpposingOptions): str
}, [isBlack, options?.dark, options?.light]);
}
export function useOpposingColorCallback(options?: TOpposingOptions): (color: string) => string {
export function useOpposingColorCallback(
options?: OpposingColorOptions,
): (color: string) => string {
const isDark = useIsDarkCallback();
return useCallback(
(color: string) => {

View file

@ -1,7 +1,7 @@
import { useCallback } from 'react';
import format from 'string-format';
import type { UseStrfArgs } from './types';
type UseStrfArgs = { [k: string]: unknown } | string;
/**
* Format a string with variables, like Python's string.format()

View file

@ -5,7 +5,20 @@ import utcPlugin from 'dayjs/plugin/utc';
import { useConfig } from '~/context';
import { isStructuredOutput } from '~/types';
import type { TTableToStringFormatter, TTableToStringFormatted } from './types';
type TableToStringFormatter =
| ((v: string) => string)
| ((v: number) => string)
| ((v: number[]) => string)
| ((v: string[]) => string)
| ((v: boolean) => string);
interface TableToStringFormatted {
age: (v: number) => string;
active: (v: boolean) => string;
as_path: (v: number[]) => string;
communities: (v: string[]) => string;
rpki_state: (v: number, n: RPKIState) => string;
}
dayjs.extend(relativeTimePlugin);
dayjs.extend(utcPlugin);
@ -65,11 +78,11 @@ export function useTableToString(
rpki_state: formatRpkiState,
};
function isFormatted(key: string): key is keyof TTableToStringFormatted {
function isFormatted(key: string): key is keyof TableToStringFormatted {
return key in tableFormatMap;
}
function getFmtFunc(accessor: keyof Route): TTableToStringFormatter {
function getFmtFunc(accessor: keyof Route): TableToStringFormatter {
if (isFormatted(accessor)) {
return tableFormatMap[accessor];
} else {

View file

@ -3,14 +3,10 @@ import { fetchWithTimeout } from '~/util';
import type {
QueryFunction,
QueryFunctionContext,
UseQueryOptions,
UseQueryResult,
UseQueryOptions,
QueryFunctionContext,
} from 'react-query';
import type { WtfIsMyIP } from '~/types';
const URL_IP4 = 'https://ipv4.json.myip.wtf';
const URL_IP6 = 'https://ipv6.json.myip.wtf';
interface WtfIndividual {
ip: string;
@ -21,6 +17,23 @@ interface WtfIndividual {
type Wtf = [UseQueryResult<WtfIndividual>, UseQueryResult<WtfIndividual>, () => Promise<void>];
/**
* myip.wtf response.
*
* @see https://github.com/wtfismyip/wtfismyip
* @see https://wtfismyip.com/automation
*/
interface WtfIsMyIP {
YourFuckingIPAddress: string;
YourFuckingLocation: string;
YourFuckingISP: string;
YourFuckingTorExit: boolean;
YourFuckingCountryCode: string;
}
const URL_IP4 = 'https://ipv4.json.myip.wtf';
const URL_IP6 = 'https://ipv6.json.myip.wtf';
function transform(wtf: WtfIsMyIP): WtfIndividual {
const { YourFuckingIPAddress, YourFuckingISP, YourFuckingLocation, YourFuckingCountryCode } = wtf;
return {

View file

@ -6,51 +6,23 @@
"strict": true,
"baseUrl": "." /* Base directory to resolve non-absolute module names. */,
"paths": {
"~/components": [
"components/index"
],
"~/components/*": [
"components/*"
],
"~/context": [
"context/index"
],
"~/context/*": [
"context/*"
],
"~/hooks": [
"hooks/index"
],
"~/hooks/*": [
"hooks/*"
],
"~/state": [
"state/index"
],
"~/state/*": [
"state/*"
],
"~/types": [
"types/index"
],
"~/types/*": [
"types/*"
],
"~/util": [
"util/index"
],
"~/util/*": [
"util/*"
]
"~/components": ["components/index"],
"~/components/*": ["components/*"],
"~/context": ["context/index"],
"~/context/*": ["context/*"],
"~/hooks": ["hooks/index"],
"~/hooks/*": ["hooks/*"],
"~/state": ["state/index"],
"~/state/*": ["state/*"],
"~/types": ["types/index"],
"~/types/*": ["types/*"],
"~/util": ["util/index"],
"~/util/*": ["util/*"]
},
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"noEmit": true,
"moduleResolution": "node",
@ -59,10 +31,7 @@
"jsx": "preserve",
"incremental": true
},
"exclude": [
"node_modules",
".next"
],
"exclude": ["node_modules", ".next"],
"include": [
"next-env.d.ts",
"**/*.ts",

View file

@ -1,9 +0,0 @@
export interface TASNQuery {
data: {
asn: {
organization: {
orgName: string;
} | null;
};
};
}

View file

@ -1,20 +1,13 @@
export type TQueryTypes = '' | TValidQueryTypes;
export type TValidQueryTypes = 'bgp_route' | 'bgp_community' | 'bgp_aspath' | 'ping' | 'traceroute';
export interface FormData {
queryLocation: string[];
queryType: string;
queryTarget: string;
}
export interface TFormQuery extends Omit<FormData, 'queryLocation'> {
queryLocation: string;
}
export type FormQuery = Swap<FormData, 'queryLocation', string>;
export interface TStringTableData extends Omit<QueryResponse, 'output'> {
output: StructuredResponse;
}
export type StringTableData = Swap<QueryResponse, 'output', StructuredResponse>;
export interface TQueryResponseString extends Omit<QueryResponse, 'output'> {
output: string;
}
export type StringQueryResponse = Swap<QueryResponse, 'output', string>;
export type ErrorLevels = 'success' | 'warning' | 'error';

View file

@ -1,11 +1,14 @@
import type { MotionProps } from 'framer-motion';
declare global {
export declare global {
type Dict<T = string> = Record<string, T>;
type ValueOf<T> = T[keyof T];
type Nullable<T> = T | null;
type Get<T, K extends keyof T> = T[K];
type Swap<T, K extends keyof T, V> = Record<K, V> & Omit<T, K>;
type RPKIState = 0 | 1 | 2 | 3;
type ResponseLevel = 'success' | 'warning' | 'error' | 'danger';
@ -45,12 +48,6 @@ declare global {
output: string | StructuredResponse;
format: 'text/plain' | 'application/json';
};
type ReactRef<T = HTMLElement> = MutableRefObject<T>;
type Animated<T> = Omit<T, keyof MotionProps> &
Omit<MotionProps, keyof T> & { transition?: MotionProps['transition'] };
type MeronexIcon = import('@meronex/icons').IconBaseProps;
type RequiredProps<T> = { [P in keyof T]-?: Exclude<T[P], undefined> };

View file

@ -1,5 +1,5 @@
import type { FormData, TStringTableData, TQueryResponseString } from './data';
import type { DirectiveSelect, Directive } from './config';
import type { FormData, StringTableData, StringQueryResponse } from './data';
import type { DirectiveSelect, Directive, Link, Menu } from './config';
export function isString(a: unknown): a is string {
return typeof a === 'string';
@ -15,11 +15,11 @@ export function isObject<T extends unknown = unknown>(
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
}
export function isStructuredOutput(data: unknown): data is TStringTableData {
export function isStructuredOutput(data: unknown): data is StringTableData {
return isObject(data) && 'output' in data;
}
export function isStringOutput(data: unknown): data is TQueryResponseString {
export function isStringOutput(data: unknown): data is StringQueryResponse {
return (
isObject(data) && 'output' in data && typeof (data as { output: unknown }).output === 'string'
);
@ -38,3 +38,11 @@ export function isQueryField(field: string): field is keyof FormData {
export function isSelectDirective(directive: Directive): directive is DirectiveSelect {
return directive.fieldType === 'select';
}
export function isLink(item: Link | Menu): item is Link {
return 'url' in item;
}
export function isMenu(item: Link | Menu): item is Menu {
return 'content' in item;
}

View file

@ -1,4 +1,3 @@
export * from './caida';
export * from './common';
export * from './config';
export * from './data';
@ -6,5 +5,3 @@ export * from './dns-over-https';
export * from './guards';
export * from './table';
export * from './theme';
export * from './util';
export * from './wtfismyip';

View file

@ -1,13 +1,13 @@
import type { CellProps } from 'react-table';
export interface TColumn {
export interface TableColumn {
Header: string;
accessor: keyof Route;
align: string;
hidden: boolean;
}
export type TCellRender = {
export type CellRenderProps = {
column: CellProps<RouteField>['column'];
row: CellProps<RouteField>['row'];
value: CellProps<RouteField>['value'];

View file

@ -1,4 +0,0 @@
export interface PathPart {
base: number;
children: PathPart[];
}

View file

@ -1,13 +0,0 @@
/**
* myip.wtf response.
*
* @see https://github.com/wtfismyip/wtfismyip
* @see https://wtfismyip.com/automation
*/
export interface WtfIsMyIP {
YourFuckingIPAddress: string;
YourFuckingLocation: string;
YourFuckingISP: string;
YourFuckingTorExit: boolean;
YourFuckingCountryCode: string;
}