continue typescript & chakra v1 migrations [skip ci]

This commit is contained in:
checktheroads 2020-12-11 09:55:21 -07:00
parent 781a2608a0
commit be601e4aef
43 changed files with 2042 additions and 2083 deletions

View file

@ -1,41 +0,0 @@
import * as React from 'react';
import { useEffect } from 'react';
import { Text } from '@chakra-ui/core';
import { components } from 'react-select';
import { ChakraSelect } from 'app/components';
export const CommunitySelect = ({ name, communities, onChange, register, unregister }) => {
const communitySelections = communities.map(c => {
return {
value: c.community,
label: c.display_name,
description: c.description,
};
});
const Option = ({ label, data, ...props }) => {
return (
<components.Option {...props}>
<Text>{label}</Text>
<Text fontSize="xs" as="span">
{data.description}
</Text>
</components.Option>
);
};
useEffect(() => {
register({ name });
return () => unregister(name);
}, [name, register, unregister]);
return (
<ChakraSelect
innerRef={register}
size="lg"
name={name}
onChange={e => {
onChange({ field: name, value: e.value || '' });
}}
options={communitySelections}
components={{ Option }}
/>
);
};

View file

@ -1,69 +0,0 @@
import * as React from 'react';
import { useEffect } from 'react';
import { Input, useColorMode } from '@chakra-ui/core';
const fqdnPattern = /^(?!:\/\/)([a-zA-Z0-9-]+\.)?[a-zA-Z0-9-][a-zA-Z0-9-]+\.[a-zA-Z-]{2,6}?$/gim;
const bg = { dark: 'whiteAlpha.100', light: 'white' };
const color = { dark: 'whiteAlpha.800', light: 'gray.400' };
const border = { dark: 'whiteAlpha.50', light: 'gray.100' };
const placeholderColor = { dark: 'whiteAlpha.700', light: 'gray.600' };
export const QueryTarget = ({
placeholder,
register,
unregister,
setFqdn,
name,
value,
setTarget,
resolveTarget,
displayValue,
setDisplayValue,
}) => {
const { colorMode } = useColorMode();
const handleBlur = () => {
if (resolveTarget && displayValue && fqdnPattern.test(displayValue)) {
setFqdn(displayValue);
} else if (resolveTarget && !displayValue) {
setFqdn(false);
}
};
const handleChange = e => {
setDisplayValue(e.target.value);
setTarget({ field: name, value: e.target.value });
};
const handleKeyDown = e => {
if ([9, 13].includes(e.keyCode)) {
handleBlur();
}
};
useEffect(() => {
register({ name });
return () => unregister(name);
}, [register, unregister, name]);
return (
<>
<input hidden readOnly name={name} ref={register} value={value} />
<Input
size="lg"
aria-label={placeholder}
name="query_target_display"
bg={bg[colorMode]}
onBlur={handleBlur}
onFocus={handleBlur}
onKeyDown={handleKeyDown}
value={displayValue}
borderRadius="0.25rem"
onChange={handleChange}
color={color[colorMode]}
placeholder={placeholder}
borderColor={border[colorMode]}
_placeholder={{
color: placeholderColor[colorMode],
}}
/>
</>
);
};

View file

@ -1,19 +0,0 @@
import * as React from 'react';
import { ChakraSelect } from 'app/components';
export const QueryType = ({ queryTypes, onChange, label }) => {
const queries = queryTypes
.filter(q => q.enable === true)
.map(q => {
return { value: q.name, label: q.display_name };
});
return (
<ChakraSelect
size="lg"
name="query_type"
onChange={e => onChange({ field: 'query_type', value: e.value })}
options={queries}
aria-label={label}
/>
);
};

View file

@ -1,12 +0,0 @@
import * as React from 'react';
import { ChakraSelect } from 'app/components';
export const QueryVrf = ({ vrfs, onChange, label }) => (
<ChakraSelect
size="lg"
options={vrfs}
name="query_vrf"
aria-label={label}
onChange={e => onChange({ field: 'query_vrf', value: e.value })}
/>
);

View file

@ -1,145 +0,0 @@
import * as React from 'react';
import { forwardRef, useEffect } from 'react';
import { Button, Icon, Spinner, Stack, Tag, Text, Tooltip, useColorMode } from '@chakra-ui/core';
import useAxios from 'axios-hooks';
import format from 'string-format';
import { useConfig } from 'app/context';
format.extend(String.prototype, {});
const labelBg = { dark: 'secondary', light: 'secondary' };
const labelBgSuccess = { dark: 'success', light: 'success' };
export const ResolvedTarget = forwardRef(
({ fqdnTarget, setTarget, queryTarget, families, availVrfs }, ref) => {
const { colorMode } = useColorMode();
const config = useConfig();
const labelBgStatus = {
true: labelBgSuccess[colorMode],
false: labelBg[colorMode],
};
const dnsUrl = config.web.dns_provider.url;
const query4 = families.includes(4);
const query6 = families.includes(6);
const params = {
4: {
url: dnsUrl,
params: { name: fqdnTarget, type: 'A' },
headers: { accept: 'application/dns-json' },
crossdomain: true,
timeout: 1000,
},
6: {
url: dnsUrl,
params: { name: fqdnTarget, type: 'AAAA' },
headers: { accept: 'application/dns-json' },
crossdomain: true,
timeout: 1000,
},
};
const [{ data: data4, loading: loading4, error: error4 }] = useAxios(params[4]);
const [{ data: data6, loading: loading6, error: error6 }] = useAxios(params[6]);
const handleOverride = overridden => {
setTarget({ field: 'query_target', value: overridden });
};
const isSelected = value => {
return labelBgStatus[value === queryTarget];
};
const findAnswer = data => {
return data?.Answer?.filter(answerData => answerData.type === data?.Question[0]?.type)[0]
?.data;
};
useEffect(() => {
if (query6 && data6?.Answer) {
handleOverride(findAnswer(data6));
} else if (query4 && data4?.Answer && !query6 && !data6?.Answer) {
handleOverride(findAnswer(data4));
} else if (query4 && data4?.Answer) {
handleOverride(findAnswer(data4));
}
}, [data4, data6]);
return (
<Stack
ref={ref}
isInline
w="100%"
justifyContent={
query4 && data4?.Answer && query6 && data6?.Answer && availVrfs.length > 1
? 'space-between'
: 'flex-end'
}
flexWrap="wrap">
{loading4 ||
error4 ||
(query4 && findAnswer(data4) && (
<Tag my={2}>
<Tooltip
hasArrow
label={config.web.text.fqdn_tooltip.format({
protocol: 'IPv4',
})}
placement="bottom">
<Button
height="unset"
minW="unset"
fontSize="xs"
py="0.1rem"
px={2}
mr={2}
variantColor={labelBgStatus[findAnswer(data4) === queryTarget]}
borderRadius="md"
onClick={() => handleOverride(findAnswer(data4))}>
IPv4
</Button>
</Tooltip>
{loading4 && <Spinner />}
{error4 && <Icon name="warning" />}
{findAnswer(data4) && (
<Text fontSize="xs" fontFamily="mono" as="span" fontWeight={400}>
{findAnswer(data4)}
</Text>
)}
</Tag>
))}
{loading6 ||
error6 ||
(query6 && findAnswer(data6) && (
<Tag my={2}>
<Tooltip
hasArrow
label={config.web.text.fqdn_tooltip.format({
protocol: 'IPv6',
})}
placement="bottom">
<Button
height="unset"
minW="unset"
fontSize="xs"
py="0.1rem"
px={2}
mr={2}
variantColor={isSelected(findAnswer(data6))}
borderRadius="md"
onClick={() => handleOverride(findAnswer(data6))}>
IPv6
</Button>
</Tooltip>
{loading6 && <Spinner />}
{error6 && <Icon name="warning" />}
{findAnswer(data6) && (
<Text fontSize="xs" fontFamily="mono" as="span" fontWeight={400}>
{findAnswer(data6)}
</Text>
)}
</Tag>
))}
</Stack>
);
},
);

View file

@ -15,12 +15,12 @@ export type TButtonSizeMap = {
};
export interface TSubmitButton extends BoxProps {
isLoading: boolean;
isDisabled: boolean;
isActive: boolean;
isFullWidth: boolean;
size: keyof TButtonSizeMap;
loadingText: string;
isLoading?: boolean;
isDisabled?: boolean;
isActive?: boolean;
isFullWidth?: boolean;
size?: keyof TButtonSizeMap;
loadingText?: string;
}
export interface TRequeryButton extends ButtonProps {

View file

@ -1,4 +1,4 @@
import { Text } from '@chakra-ui/core';
import { Text } from '@chakra-ui/react';
import ReactCountdown, { zeroPad } from 'react-countdown';
import { If } from '~/components';
import { useColorValue } from '~/context';
@ -13,10 +13,10 @@ const Renderer = (props: IRenderer) => {
const bg = useColorValue('black', 'white');
return (
<>
<If condition={completed}>
<If c={completed}>
<Text fontSize="xs" />
</If>
<If condition={!completed}>
<If c={!completed}>
<Text fontSize="xs" color="gray.500">
{text}
<Text as="span" fontSize="xs" color={bg}>

View file

@ -0,0 +1,56 @@
import { useEffect, useMemo } from 'react';
import { Text } from '@chakra-ui/react';
import { components } from 'react-select';
import { Select } from '~/components';
import type { OptionProps } from 'react-select';
import type { TBGPCommunity, TSelectOption } from '~/types';
import type { TCommunitySelect } from './types';
function buildOptions(communities: TBGPCommunity[]): TSelectOption[] {
return communities.map(c => ({
value: c.community,
label: c.display_name,
description: c.description,
}));
}
const Option = (props: OptionProps<Dict>) => {
const { label, data } = props;
return (
<components.Option {...props}>
<Text as="span">{label}</Text>
<Text fontSize="xs" as="span">
{data.description}
</Text>
</components.Option>
);
};
export const CommunitySelect = (props: TCommunitySelect) => {
const { name, communities, onChange, register, unregister } = props;
const options = useMemo(() => buildOptions(communities), [communities.length]);
function handleChange(e: TSelectOption | TSelectOption[]): void {
if (!Array.isArray(e)) {
onChange({ field: name, value: e.value });
}
}
useEffect(() => {
register({ name });
return () => unregister(name);
}, [name, register, unregister]);
return (
<Select
size="lg"
name={name}
options={options}
innerRef={register}
onChange={handleChange}
components={{ Option }}
/>
);
};

View file

@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { Flex, FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react';
import { If } from '~/components';
import { useColorValue } from '~/context';
@ -6,9 +7,29 @@ import { useBooleanValue } from '~/hooks';
import { TField } from './types';
export const FormField = (props: TField) => {
const { label, name, error, hiddenLabels, labelAddOn, fieldAddOn, children, ...rest } = props;
const {
name,
label,
errors,
children,
labelAddOn,
fieldAddOn,
hiddenLabels = false,
...rest
} = props;
const labelColor = useColorValue('blackAlpha.700', 'whiteAlpha.700');
const opacity = useBooleanValue(hiddenLabels, 0, undefined);
const error = useMemo<string | undefined>(() => {
let result;
if (Array.isArray(errors)) {
result = errors.join(', ');
} else if (typeof errors === 'string') {
result = errors;
}
return result;
}, [errors]);
return (
<FormControl
mx={2}
@ -38,7 +59,7 @@ export const FormField = (props: TField) => {
{fieldAddOn}
</Flex>
</If>
<FormErrorMessage opacity={opacity}>{error && error.message}</FormErrorMessage>
<FormErrorMessage opacity={opacity}>{error}</FormErrorMessage>
</FormControl>
);
};

View file

@ -1 +1,8 @@
export * from './communitySelect';
export * from './field';
export * from './queryLocation';
export * from './queryTarget';
export * from './queryType';
export * from './queryVrf';
export * from './resolvedTarget';
export * from './row';

View file

@ -1,12 +1,9 @@
import { useMemo } from 'react';
import { Select } from '~/components';
import { useConfig } from '~/context';
import type { TNetwork } from '~/types';
import type { TQueryLocation, OnChangeArgs } from './types';
function isOnChangeArgsArray(e: OnChangeArgs | OnChangeArgs[]): e is OnChangeArgs[] {
return Array.isArray(e);
}
import type { TNetwork, TSelectOption } from '~/types';
import type { TQuerySelectField } from './types';
function buildOptions(networks: TNetwork[]) {
return networks.map(net => {
@ -20,15 +17,16 @@ function buildOptions(networks: TNetwork[]) {
});
}
export const QueryLocation = (props: TQueryLocation) => {
const { locations, onChange, label } = props;
export const QueryLocation = (props: TQuerySelectField) => {
const { onChange, label } = props;
const options = useMemo(() => buildOptions(locations), [locations.length]);
const { networks } = useConfig();
const options = useMemo(() => buildOptions(networks), [networks.length]);
function handleChange(e: OnChangeArgs | OnChangeArgs[]): void {
if (isOnChangeArgsArray(e)) {
const value = e.map(sel => sel.value as string);
onChange({ label: 'query_location', value });
function handleChange(e: TSelectOption): void {
if (Array.isArray(e.value)) {
const value = e.value.map(sel => sel);
onChange({ field: 'query_location', value });
}
}

View file

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

View file

@ -0,0 +1,33 @@
import { useMemo } from 'react';
import { Select } from '~/components';
import { useConfig } from '~/context';
import type { TQuery, TSelectOption } from '~/types';
import type { TQuerySelectField } from './types';
function buildOptions(queryTypes: TQuery[]): TSelectOption[] {
return queryTypes
.filter(q => q.enable === true)
.map(q => ({ value: q.name, label: q.display_name }));
}
export const QueryType = (props: TQuerySelectField) => {
const { onChange, label } = props;
const { queries } = useConfig();
const options = useMemo(() => buildOptions(queries.list), [queries.list.length]);
function handleChange(e: TSelectOption): void {
onChange({ field: 'query_type', value: e.value });
}
return (
<Select
size="lg"
name="query_type"
options={options}
aria-label={label}
onChange={handleChange}
/>
);
};

View file

@ -0,0 +1,29 @@
import { useMemo } from 'react';
import { Select } from '~/components';
import { TDeviceVrf, TSelectOption } from '~/types';
import type { TQueryVrf } from './types';
function buildOptions(queryVrfs: TDeviceVrf[]): TSelectOption[] {
return queryVrfs.map(q => ({ value: q.id, label: q.display_name }));
}
export const QueryVrf = (props: TQueryVrf) => {
const { vrfs, onChange, label } = props;
const options = useMemo(() => buildOptions(vrfs), [vrfs.length]);
function handleChange(e: TSelectOption): void {
onChange({ field: 'query_vrf', value: e.value });
}
return (
<Select
size="lg"
name="query_vrf"
options={options}
aria-label={label}
onChange={handleChange}
/>
);
};

View file

@ -0,0 +1,146 @@
import { useEffect } from 'react';
import { Button, Icon, Spinner, Stack, Tag, Text, Tooltip } from '@chakra-ui/react';
import { useQuery } from 'react-query';
import { useConfig } from '~/context';
import { useStrf } from '~/hooks';
import type { DnsOverHttps, ColorNames } from '~/types';
import type { TResolvedTarget } from './types';
function findAnswer(data: DnsOverHttps.Response | undefined): string {
let answer = '';
if (typeof data !== 'undefined') {
answer = data?.Answer?.filter(answerData => answerData.type === data?.Question[0]?.type)[0]
?.data;
}
return answer;
}
export const ResolvedTarget = (props: TResolvedTarget) => {
const { fqdnTarget, setTarget, queryTarget, families, availVrfs } = props;
const { web } = useConfig();
const dnsUrl = web.dns_provider.url;
const query4 = Array.from(families).includes(4);
const query6 = Array.from(families).includes(6);
const tooltip4 = useStrf(web.text.fqdn_tooltip, { protocol: 'IPv4' });
const tooltip6 = useStrf(web.text.fqdn_tooltip, { protocol: 'IPv6' });
const { data: data4, isLoading: isLoading4, isError: isError4 } = useQuery(
[fqdnTarget, 4],
dnsQuery,
);
const { data: data6, isLoading: isLoading6, isError: isError6 } = useQuery(
[fqdnTarget, 6],
dnsQuery,
);
async function dnsQuery(
target: string,
family: 4 | 6,
): Promise<DnsOverHttps.Response | undefined> {
let json;
const type = family === 4 ? 'A' : family === 6 ? 'AAAA' : '';
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1000);
const res = await fetch(`${dnsUrl}?name=${target}&type=${type}`, {
headers: { accept: 'application/dns-json' },
signal: controller.signal,
mode: 'cors',
});
json = await res.json();
clearTimeout(timeout);
return json;
}
function handleOverride(value: string): void {
setTarget({ field: 'query_target', value });
}
function isSelected(value: string): ColorNames {
if (value === queryTarget) {
return 'success';
} else {
return 'secondary';
}
}
useEffect(() => {
if (query6 && data6?.Answer) {
handleOverride(findAnswer(data6));
} else if (query4 && data4?.Answer && !query6 && !data6?.Answer) {
handleOverride(findAnswer(data4));
} else if (query4 && data4?.Answer) {
handleOverride(findAnswer(data4));
}
}, [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>
);
};

View file

@ -0,0 +1,15 @@
import { Flex } from '@chakra-ui/react';
import { FlexProps } from '@chakra-ui/react';
export const FormRow = (props: FlexProps) => {
return (
<Flex
w="100%"
flexDir="row"
flexWrap="wrap"
justifyContent={{ base: 'center', lg: 'space-between' }}
{...props}
/>
);
};

View file

@ -1,20 +1,67 @@
import type { FormControlProps } from '@chakra-ui/react';
import type { FieldError } from 'react-hook-form';
import type { TNetwork } from '~/types';
import type { FieldError, Control } from 'react-hook-form';
import type { TDeviceVrf, TBGPCommunity, OnChangeArgs, Families, TFormData } from '~/types';
export interface TField extends FormControlProps {
name: string;
label: string;
error?: FieldError;
hiddenLabels: boolean;
errors?: FieldError | FieldError[];
hiddenLabels?: boolean;
labelAddOn?: React.ReactNode;
fieldAddOn?: React.ReactNode;
}
export type OnChangeArgs = { label: string; value: string | string[] };
export type OnChange = (f: OnChangeArgs) => void;
export interface TQueryLocation {
locations: TNetwork[];
onChange(f: OnChangeArgs | OnChangeArgs[]): void;
export interface TQuerySelectField {
onChange: OnChange;
label: string;
}
export interface TQueryVrf extends TQuerySelectField {
vrfs: TDeviceVrf[];
}
export interface TCommunitySelect {
name: string;
onChange: OnChange;
communities: TBGPCommunity[];
register: Control['register'];
unregister: Control['unregister'];
}
/**
* placeholder,
register,
unregister,
setFqdn,
name,
value,
setTarget,
resolveTarget,
displayValue,
setDisplayValue,
*/
export interface TQueryTarget {
name: string;
placeholder: string;
displayValue: string;
resolveTarget: boolean;
setFqdn(f: string | null): void;
setTarget(e: OnChangeArgs): void;
register: Control['register'];
value: TFormData['query_target'];
setDisplayValue(d: string): void;
unregister: Control['unregister'];
}
export interface TResolvedTarget {
families: Families;
queryTarget: string;
availVrfs: TDeviceVrf[];
fqdnTarget: string | null;
setTarget(e: OnChangeArgs): void;
}

View file

@ -1,11 +1,11 @@
import { Flex } from '@chakra-ui/react';
import { motion, AnimatePresence } from 'framer-motion';
import { useColorValue, useConfig, useGlobalState, useBreakpointValue } from '~/context';
import { AnimatedDiv, Title, ResetButton, ColorModeToggle } from '~/components';
import { useColorValue, useConfig, useGlobalState, useBreakpointValue } from '~/context';
import { useBooleanValue } from '~/hooks';
import type { ResponsiveValue } from '@chakra-ui/react';
import type { THeader, TTitleMode } from './types';
import type { THeader, TTitleMode, THeaderLayout } from './types';
const headerTransition = {
type: 'spring',
@ -139,7 +139,10 @@ export const Header = (props: THeader) => {
lg: [resetButton, title, colorModeToggle],
xl: [resetButton, title, colorModeToggle],
},
);
) as THeaderLayout;
const layoutBp: keyof THeaderLayout =
useBreakpointValue({ base: 'sm', md: 'md', lg: 'lg', xl: 'xl' }) ?? 'sm';
return (
<Flex
@ -158,7 +161,7 @@ export const Header = (props: THeader) => {
justify="space-between"
flex="1 0 auto"
alignItems={isSubmitting ? 'center' : 'flex-start'}>
{layout}
{layout[layoutBp]}
</Flex>
</Flex>
);

View file

@ -7,3 +7,10 @@ export interface THeader extends FlexProps {
}
export type TTitleMode = IConfig['web']['text']['title_mode'];
export type THeaderLayout = {
sm: [JSX.Element, JSX.Element, JSX.Element];
md: [JSX.Element, JSX.Element, JSX.Element];
lg: [JSX.Element, JSX.Element, JSX.Element];
xl: [JSX.Element, JSX.Element, JSX.Element];
};

View file

@ -1,7 +1,6 @@
export * from './buttons';
export * from './card';
export * from './codeBlock';
export * from './CommunitySelect';
export * from './countdown';
export * from './debugger';
export * from './footer';
@ -9,17 +8,13 @@ export * from './form';
export * from './greeting';
export * from './header';
export * from './help';
export * from './HyperglassForm';
export * from './label';
export * from './layout';
export * from './loading';
export * from './lookingGlass';
export * from './markdown';
export * from './meta';
export * from './output';
export * from './QueryTarget';
export * from './QueryType';
export * from './QueryVrf';
export * from './ResolvedTarget';
export * from './results';
export * from './select';
export * from './table';

View file

@ -1,7 +1,9 @@
import { Flex, Spinner } from '@chakra-ui/react';
import { useColorValue } from '~/context';
export const Loading: React.FC = () => {
import type { LoadableBaseOptions } from 'next/dynamic';
export const Loading: LoadableBaseOptions['loading'] = () => {
const bg = useColorValue('white', 'black');
const color = useColorValue('black', 'white');
return (

View file

@ -0,0 +1,335 @@
import { useEffect, useMemo, useState } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { uniqWith } from 'lodash';
import * as yup from 'yup';
import {
FormRow,
QueryVrf,
FormField,
HelpModal,
QueryType,
QueryTarget,
SubmitButton,
QueryLocation,
ResolvedTarget,
CommunitySelect,
} from '~/components';
import { useConfig, useGlobalState } from '~/context';
import { useStrf, useGreeting } from '~/hooks';
import type { Families, TFormData, TDeviceVrf, TQueryTypes, OnChangeArgs } from '~/types';
function isString(a: any): a is string {
return typeof a === 'string';
}
function isQueryType(q: any): q is TQueryTypes {
let result = false;
if (
typeof q === 'string' &&
['bgp_route', 'bgp_community', 'bgp_aspath', 'ping', 'traceroute'].includes(q)
) {
result = true;
}
return result;
}
export const HyperglassForm = () => {
const { web, content, devices, messages, networks, queries } = useConfig();
const { formData, isSubmitting } = useGlobalState();
const [greetingAck, setGreetingAck] = useGreeting();
const noQueryType = useStrf(messages.no_input, { field: web.text.query_type });
const noQueryLoc = useStrf(messages.no_input, { field: web.text.query_location });
const noQueryTarget = useStrf(messages.no_input, { field: web.text.query_target });
const formSchema = yup.object().shape({
query_location: yup.array().of(yup.string()).required(noQueryLoc),
query_type: yup.string().required(noQueryType),
query_vrf: yup.string(),
query_target: yup.string().required(noQueryTarget),
});
const { handleSubmit, register, unregister, setValue, errors } = useForm<TFormData>({
validationSchema: formSchema,
defaultValues: { query_vrf: 'default', query_target: '' },
});
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>([]);
function onSubmit(values: TFormData): void {
if (!greetingAck && web.greeting.required) {
window.location.reload(false);
setGreetingAck(false);
} else {
formData.set(values);
isSubmitting.set(true);
}
}
/*
const handleLocChange = locObj => {
setQueryLocation(locObj.value);
const allVrfs = [];
const deviceVrfs = [];
locObj.value.map(loc => {
const locVrfs = [];
config.devices[loc].vrfs.map(vrf => {
locVrfs.push({
label: vrf.display_name,
value: vrf.id,
});
deviceVrfs.push([{ id: vrf.id, ipv4: vrf.ipv4, ipv6: vrf.ipv6 }]);
});
allVrfs.push(locVrfs);
});
deviceVrfs.length !== 0 &&
intersecting.length !== 0 &&
deviceVrfs
.filter(v => intersecting.every(i => i.id === v.id))
.reduce((a, b) => a.concat(b))
.filter(v => v.id === 'default')
.map(v => {
v.ipv4 === true && ipv4++;
v.ipv6 === true && ipv6++;
});
*/
// function handleLocChange(locObj: TSelectOption) {
// const allVrfs = [] as TDeviceVrf[][];
// const deviceVrfs = [] as TDeviceVrf[][];
// if (Array.isArray(locObj.value)) {
// setQueryLocation(locObj.value);
// for (const loc of locObj.value) {
// const locVrfs = [] as TDeviceVrf[];
// for (const vrf of devices.filter(dev => dev.name === loc)[0].vrfs) {
// locVrfs.push(vrf);
// deviceVrfs.push([vrf]);
// }
// allVrfs.push(locVrfs);
// }
// }
// // Use _.intersectionWith to create an array of VRFs common to all selected locations.
// const intersecting: TDeviceVrf[] = intersectionWith(...allVrfs, isEqual);
// setAvailVrfs(intersecting);
// // If there are no intersecting VRFs, use the default VRF.
// if (intersecting.filter(i => i.id === queryVrf).length === 0 && queryVrf !== 'default') {
// setQueryVrf('default');
// }
// let ipv4 = 0;
// let ipv6 = 0;
// if (deviceVrfs.length !== 0 && intersecting.length !== 0) {
// const matching = deviceVrfs
// // Select intersecting VRFs
// .filter(v => intersecting.every(i => i.id === v.id))
// .reduce((a, b) => a.concat(b))
// .filter(v => v.id === 'default');
// for (const match of matching) {
// if (match.ipv4) {
// ipv4++;
// }
// if (match.ipv6) {
// ipv6++;
// }
// }
// }
// if (ipv4 !== 0 && ipv4 === ipv6) {
// setFamilies([4, 6]);
// } else if (ipv4 > ipv6) {
// setFamilies([4]);
// } else if (ipv4 < ipv6) {
// setFamilies([6]);
// } else {
// setFamilies([]);
// }
// }
function handleLocChange(locations: string | string[]): void {
const allVrfs = [] as TDeviceVrf[];
if (Array.isArray(locations)) {
setQueryLocation(locations);
for (const loc of locations) {
for (const vrf of devices.filter(dev => dev.name === loc)[0].vrfs) {
allVrfs.push(vrf);
}
}
}
// Use _.intersectionWith to create an array of VRFs common to all selected locations.
const intersecting = uniqWith<TDeviceVrf>(allVrfs, (a, b) => a.id === b.id);
setAvailVrfs(intersecting);
// If there are no intersecting VRFs, use the default VRF.
if (intersecting.filter(i => i.id === queryVrf).length === 0 && queryVrf !== 'default') {
setQueryVrf('default');
}
let ipv4 = 0;
let ipv6 = 0;
if (intersecting.length !== 0) {
for (const intersection of intersecting) {
if (intersection.ipv4) {
ipv4++;
}
if (intersection.ipv6) {
ipv6++;
}
}
}
if (ipv4 !== 0 && ipv4 === ipv6) {
setFamilies([4, 6]);
} else if (ipv4 > ipv6) {
setFamilies([4]);
} else if (ipv4 < ipv6) {
setFamilies([6]);
} else {
setFamilies([]);
}
}
function handleChange(e: OnChangeArgs): void {
setValue(e.field, e.value);
if (e.field === 'query_location') {
handleLocChange(e.value);
} else if (e.field === 'query_type' && isQueryType(e.value)) {
setQueryType(e.value);
} else if (e.field === 'query_vrf' && isString(e.value)) {
setQueryVrf(e.value);
} else if (e.field === 'query_target' && isString(e.value)) {
setQueryTarget(e.value);
}
}
const vrfContent = useMemo(() => {
if (Object.keys(content.vrf).includes(queryVrf) && queryType !== '') {
return content.vrf[queryVrf][queryType];
}
}, [queryVrf]);
const isFqdnQuery = useMemo(() => {
return ['bgp_route', 'ping', 'traceroute'].includes(queryType);
}, [queryType]);
const fqdnQuery = useMemo(() => {
let result = null;
if (fqdnTarget && queryVrf === 'default' && fqdnTarget) {
result = fqdnTarget;
}
return result;
}, [queryVrf, queryType]);
useEffect(() => {
register({ name: 'query_location' });
register({ name: 'query_type' });
register({ name: 'query_vrf' });
}, [register]);
Object.keys(errors).length >= 1 && console.error(errors);
return (
<Box
p={0}
my={4}
w="100%"
as="form"
mx="auto"
textAlign="left"
onSubmit={handleSubmit(onSubmit)}
maxW={{ base: '100%', lg: '75%' }}>
<FormRow>
<FormField
name="query_location"
errors={errors.query_location}
label={web.text.query_location}>
<QueryLocation onChange={handleChange} label={web.text.query_location} />
</FormField>
<FormField
name="query_type"
errors={errors.query_type}
label={web.text.query_type}
labelAddOn={vrfContent && <HelpModal item={vrfContent} name="query_type" />}>
<QueryType onChange={handleChange} label={web.text.query_type} />
</FormField>
</FormRow>
<FormRow>
{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} />
</FormField>
)}
<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}
/>
)
}>
{queryType === 'bgp_community' && queries.bgp_community.mode === 'select' ? (
<CommunitySelect
name="query_target"
register={register}
unregister={unregister}
onChange={handleChange}
communities={queries.bgp_community.communities}
/>
) : (
<QueryTarget
name="query_target"
register={register}
value={queryTarget}
unregister={unregister}
setFqdn={setFqdnTarget}
setTarget={handleChange}
resolveTarget={isFqdnQuery}
displayValue={displayTarget}
setDisplayValue={setDisplayTarget}
placeholder={web.text.query_target}
/>
)}
</FormField>
</FormRow>
<FormRow mt={0} justifyContent="flex-end">
<Flex
my={2}
w="100%"
ml="auto"
maxW="100%"
flex="0 0 0"
flexDir="column"
mr={{ base: 0, lg: 2 }}>
<SubmitButton isLoading={isSubmitting.value} />
</Flex>
</FormRow>
</Box>
);
};

View file

@ -1,6 +1,6 @@
import { createContext, useContext, useMemo, useState } from 'react';
import { createContext, useContext, useMemo } from 'react';
import ReactSelect from 'react-select';
import { Box } from '@chakra-ui/react';
import { Box, useDisclosure } from '@chakra-ui/react';
import { useColorMode } from '~/context';
import {
useRSTheme,
@ -17,7 +17,8 @@ import {
useIndicatorSeparatorStyle,
} from './styles';
import type { TSelect, TSelectOption, TSelectContext, TBoxAsReactSelect } from './types';
import type { TSelectOption } from '~/types';
import type { TSelect, TSelectContext, TBoxAsReactSelect } from './types';
const SelectContext = createContext<TSelectContext>(Object());
export const useSelectContext = () => useContext(SelectContext);
@ -26,7 +27,8 @@ const ReactSelectAsBox = (props: TBoxAsReactSelect) => <Box as={ReactSelect} {..
export const Select = (props: TSelect) => {
const { ctl, options, multi, onSelect, ...rest } = props;
const [isOpen, setIsOpen] = useState<boolean>(false);
const { isOpen, onOpen, onClose } = useDisclosure();
const { colorMode } = useColorMode();
const selectContext = useMemo<TSelectContext>(() => ({ colorMode, isOpen }), [colorMode, isOpen]);
@ -49,18 +51,14 @@ export const Select = (props: TSelect) => {
return (
<SelectContext.Provider value={selectContext}>
<ReactSelectAsBox
as={ReactSelect}
options={options}
isMulti={multi}
onChange={handleChange}
ref={ctl}
onMenuClose={() => {
isOpen && setIsOpen(false);
}}
onMenuOpen={() => {
!isOpen && setIsOpen(true);
}}
onMenuClose={onClose}
onMenuOpen={onOpen}
options={options}
as={ReactSelect}
isMulti={multi}
theme={rsTheme}
ref={ctl}
styles={{
menuPortal,
multiValue,

View file

@ -10,34 +10,24 @@ import type {
PlaceholderProps,
} from 'react-select';
import type { BoxProps } from '@chakra-ui/react';
import type { ColorNames } from '~/types';
import type { ColorNames, TSelectOption, TSelectOptionGroup } from '~/types';
export interface TSelectState {
[k: string]: string[];
}
export type TSelectOption = {
label: string;
value: string;
};
export type TSelectOptionGroup = {
label: string;
options: TSelectOption[];
};
export type TOptions = Array<TSelectOptionGroup | TSelectOption>;
export type TBoxAsReactSelect = Omit<IReactSelect, 'isMulti' | 'onSelect' | 'onChange'> &
Omit<BoxProps, 'onChange' | 'onSelect'>;
export interface TSelect extends TBoxAsReactSelect {
options: TOptions;
export interface TSelectBase extends TBoxAsReactSelect {
name: string;
required?: boolean;
multi?: boolean;
onSelect?: (v: TSelectOption[]) => void;
onChange?: (c: TSelectOption | TSelectOption[]) => void;
options: TOptions;
required?: boolean;
onSelect?: (s: TSelectOption) => void;
onChange?: (c: TSelectOption) => void;
colorScheme?: ColorNames;
}

View file

@ -1,3 +1,2 @@
export * from './HyperglassProvider';
export * from './MediaProvider';
export * from './GlobalState';

View file

@ -1,4 +1,4 @@
import type { IConfig, IFormData } from '~/types';
import type { IConfig, TFormData } from '~/types';
export interface THyperglassProvider {
config: IConfig;
@ -7,5 +7,5 @@ export interface THyperglassProvider {
export interface TGlobalState {
isSubmitting: boolean;
formData: IFormData;
formData: TFormData;
}

View file

@ -7,4 +7,4 @@ export interface TStringTableData extends Omit<TQueryResponse, 'output'> {
output: TStructuredResponse;
}
export type TUseGreetingReturn = [boolean, () => void];
export type TUseGreetingReturn = [boolean, (v?: boolean) => void];

View file

@ -5,11 +5,13 @@ import type { TUseGreetingReturn } from './types';
export function useGreeting(): TUseGreetingReturn {
const state = useState<boolean>(false);
state.attach(Persistence('plugin-persisted-data-key'));
if (typeof window !== 'undefined') {
state.attach(Persistence('hyperglass-greeting'));
}
function setAck(): void {
function setAck(v: boolean = true): void {
if (!state.get()) {
state.set(true);
state.set(v);
}
return;
}

View file

@ -10,12 +10,13 @@
"build": "next build && next export -o ../hyperglass/static/ui",
"start": "next start",
"clean": "rimraf --no-glob ./.next ./out",
"typecheck": "tsc",
"check:es:build": "es-check es5 './.next/static/**/*.js' -v",
"check:es:export": "es-check es5 './out/**/*.js' -v"
},
"browserslist": "> 0.25%, not dead",
"dependencies": {
"@chakra-ui/react": "^1.0.1",
"@chakra-ui/react": "^1.0.3",
"@emotion/react": "^11.1.1",
"@emotion/styled": "^11.0.0",
"@hookstate/core": "^3.0.1",
@ -26,27 +27,30 @@
"axios-hooks": "^1.9.0",
"chroma-js": "^2.1.0",
"dayjs": "^1.8.25",
"framer-motion": "^2.9.4",
"framer-motion": "^2.9.5",
"lodash": "^4.17.15",
"next": "^9.5.4",
"react": "16.14.0",
"next": "^10.0.3",
"react": "^17.0.1",
"react-countdown": "^2.2.1",
"react-dom": "16.14.0",
"react-dom": "^17.0.1",
"react-fast-compare": "^3.2.0",
"react-hook-form": "^5.7",
"react-markdown": "^4.3.1",
"react-query": "^2.26.4",
"react-select": "^3.0.8",
"react-string-replace": "^0.4.4",
"react-table": "^7.6.2",
"string-format": "^2.0.0",
"tempy": "^0.5.0",
"yarn": "^1.22.10",
"yup": "^0.28.3"
"yup": "^0.32.8"
},
"devDependencies": {
"@types/node": "^14.11.10",
"@types/react-select": "^3.0.22",
"@types/react-table": "^7.0.25",
"@types/string-format": "^2.0.0",
"@types/yup": "^0.29.9",
"@typescript-eslint/eslint-plugin": "^2.24.0",
"@typescript-eslint/parser": "^2.24.0",
"@upstatement/eslint-config": "^0.4.3",

View file

@ -1,12 +1,22 @@
import * as React from 'react';
import Head from 'next/head';
import { HyperglassProvider } from '~/context';
import { IConfig } from '~/types';
// import { useRouter } from "next/router";
import { HyperglassProvider } from 'app/context';
// import Error from "./_error";
const config = process.env._HYPERGLASS_CONFIG_;
import type { AppProps, AppInitialProps } from 'next/app';
const Hyperglass = ({ Component, pageProps }) => {
type TAppProps = AppProps & AppInitialProps;
interface TApp extends TAppProps {
appProps: { config: IConfig };
}
type TAppInitial = Pick<TApp, 'appProps'>;
const App = (props: TApp) => {
const { Component, pageProps, appProps } = props;
const { config } = appProps;
// const { asPath } = useRouter();
// if (asPath === "/structured") {
// return <Error msg="/structured" statusCode={404} />;
@ -29,4 +39,9 @@ const Hyperglass = ({ Component, pageProps }) => {
);
};
export default Hyperglass;
App.getInitialProps = async (): Promise<TAppInitial> => {
const config = (process.env._HYPERGLASS_CONFIG_ as unknown) as IConfig;
return { appProps: { config } };
};
export default App;

View file

@ -1,8 +1,8 @@
import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import type { DocumentContext } from 'next/document';
class MyDocument extends Document {
static async getInitialProps(ctx) {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
@ -11,10 +11,10 @@ class MyDocument extends Document {
return (
<Html lang="en">
<Head>
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="true" />
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="preconnect" href="https://www.google-analytics.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="true" />
</Head>
<body>
<script src="noflash.js" />

View file

@ -1,88 +0,0 @@
import React from 'react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import {
Button,
CSSReset,
Flex,
Heading,
Text,
ThemeProvider,
useColorMode,
theme as defaultTheme,
} from '@chakra-ui/core';
import { inRange } from 'lodash';
const ColorModeProvider = dynamic(
() => import('@chakra-ui/core').then(mod => mod.ColorModeProvider),
{ ssr: false },
);
const ErrorContent = ({ msg, statusCode }) => {
const { colorMode } = useColorMode();
const bg = { light: 'white', dark: 'black' };
const baseCode = inRange(statusCode, 400, 500) ? 400 : inRange(statusCode, 500, 600) ? 500 : 400;
const errorColor = {
400: { light: 'error.500', dark: 'error.300' },
500: { light: 'danger.500', dark: 'danger.300' },
};
const variantColor = {
400: 'error',
500: 'danger',
};
const color = { light: 'black', dark: 'white' };
const { push } = useRouter();
const handleClick = () => push('/');
return (
<Flex
w="100%"
minHeight="100vh"
bg={bg[colorMode]}
flexDirection="column"
color={color[colorMode]}>
<Flex
px={2}
py={0}
w="100%"
as="main"
flexGrow={1}
flexShrink={1}
flexBasis="auto"
textAlign="center"
alignItems="center"
flexDirection="column"
justifyContent="start"
mt={['50%', '50%', '50%', '25%']}>
<Heading mb={4} as="h1" fontSize="2xl">
<Text as="span" color={errorColor[baseCode][colorMode]}>
{msg}
</Text>
{statusCode === 404 && <Text as="span"> isn't a thing...</Text>}
</Heading>
<Button variant="outline" onClick={handleClick} variantColor={variantColor[baseCode]}>
Home
</Button>
</Flex>
</Flex>
);
};
const ErrorPage = ({ msg, statusCode }) => {
return (
<ThemeProvider theme={defaultTheme}>
<ColorModeProvider>
<CSSReset />
<ErrorContent msg={msg} statusCode={statusCode} />
</ColorModeProvider>
</ThemeProvider>
);
};
ErrorPage.getInitialProps = ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
const msg = err ? err.message : res.req?.url || 'Error';
return { msg, statusCode };
};
export default ErrorPage;

View file

@ -0,0 +1,88 @@
import { useMemo } from 'react';
import { useRouter } from 'next/router';
import {
Flex,
Text,
theme,
Button,
Heading,
ThemeProvider,
ChakraProvider,
useColorModeValue,
} from '@chakra-ui/react';
import { inRange } from 'lodash';
import type { NextPageContext } from 'next';
interface TError {
status: string;
code: number;
}
const ErrorContent = (props: TError) => {
const { status, code } = props;
const router = useRouter();
const bg = useColorModeValue('white', 'black');
const color = useColorModeValue('black', 'white');
const error400 = useColorModeValue('error.500', 'error.300');
const error500 = useColorModeValue('danger.500', 'danger.300');
const errorColor = { 400: error400, 500: error500 };
const colorScheme = { 400: 'error', 500: 'danger' };
const baseCode = useMemo(() => {
return inRange(code, 400, 500) ? 400 : inRange(code, 500, 600) ? 500 : 400;
}, [code]);
function handleClick(): void {
router.push('/');
}
return (
<Flex w="100%" minHeight="100vh" bg={bg} flexDirection="column" color={color}>
<Flex
px={2}
py={0}
w="100%"
as="main"
flexGrow={1}
flexShrink={1}
flexBasis="auto"
textAlign="center"
alignItems="center"
flexDirection="column"
justifyContent="start"
mt={{ base: '50%', xl: '25%' }}>
<Heading mb={4} as="h1" fontSize="2xl">
<Text as="span" color={errorColor[baseCode]}>
{status}
</Text>
{code === 404 && <Text as="span"> isn't a thing...</Text>}
</Heading>
<Button variant="outline" onClick={handleClick} colorScheme={colorScheme[baseCode]}>
Home
</Button>
</Flex>
</Flex>
);
};
const ErrorPage = (props: TError) => {
const { status, code } = props;
return (
<ThemeProvider theme={theme}>
<ChakraProvider>
<ErrorContent status={status} code={code} />
</ChakraProvider>
</ThemeProvider>
);
};
ErrorPage.getInitialProps = (ctx: NextPageContext): TError => {
const { res, err } = ctx;
const code = res ? res.statusCode : err ? err.statusCode ?? 500 : 404;
const status = err ? err.message : 'Error';
return { status, code };
};
export default ErrorPage;

View file

@ -1,37 +0,0 @@
import * as React from 'react';
import Head from 'next/head';
import dynamic from 'next/dynamic';
import { Meta, Loading } from 'app/components';
const Layout = dynamic(() => import('~/components').then(i => i.Layout), {
loading: Loading,
});
const Index = ({ faviconComponents }) => {
return (
<>
<Head>
{faviconComponents.map(({ rel, href, type }, i) => (
<link rel={rel} href={href} type={type} key={i} />
))}
</Head>
<Meta />
<Layout />
</>
);
};
export async function getStaticProps(context) {
const components = process.env._HYPERGLASS_FAVICONS_.map(icon => {
const { image_format, dimensions, prefix, rel } = icon;
const src = `/images/favicons/${prefix}-${dimensions[0]}x${dimensions[1]}.${image_format}`;
return { rel, href: src, type: `image/${image_format}` };
});
return {
props: {
faviconComponents: components,
},
};
}
export default Index;

View file

@ -0,0 +1,47 @@
import Head from 'next/head';
import dynamic from 'next/dynamic';
import { Meta, Loading } from '~/components';
import type { GetStaticProps } from 'next';
import type { Favicon, FaviconComponent } from '~/types';
const Layout = dynamic<Dict>(() => import('~/components').then(i => i.Layout), {
loading: Loading,
});
interface TIndex {
favicons: FaviconComponent[];
}
const Index = (props: TIndex) => {
const { favicons } = props;
return (
<>
<Head>
{favicons.map((icon, idx) => {
const { rel, href, type } = icon;
return <link rel={rel} href={href} type={type} key={idx} />;
})}
</Head>
<Meta />
<Layout />
</>
);
};
export const getStaticProps: GetStaticProps<TIndex> = async () => {
const faviconConfig = (process.env._HYPERGLASS_FAVICONS_ as unknown) as Favicon[];
const favicons = faviconConfig.map(icon => {
let { image_format, dimensions, prefix, rel } = icon;
if (rel === null) {
rel = '';
}
const src = `/images/favicons/${prefix}-${dimensions[0]}x${dimensions[1]}.${image_format}`;
return { rel, href: src, type: `image/${image_format}` };
});
return {
props: { favicons },
};
};
export default Index;

View file

@ -0,0 +1,13 @@
export type TSelectOption = {
label: string;
value: string | string[];
};
export type TSelectOptionGroup = {
label: string;
options: TSelectOption[];
};
export type OnChangeArgs = { field: string; value: string | string[] };
export type Families = [4] | [6] | [4, 6] | [];

View file

@ -70,42 +70,42 @@ export interface IConfigWeb {
theme: IConfigTheme;
}
export interface IQuery {
export interface TQuery {
name: string;
enable: boolean;
display_name: string;
}
export interface IBGPCommunity {
export interface TBGPCommunity {
community: string;
display_name: string;
description: string;
}
export interface IQueryBGPRoute extends IQuery {}
export interface IQueryBGPASPath extends IQuery {}
export interface IQueryPing extends IQuery {}
export interface IQueryTraceroute extends IQuery {}
export interface IQueryBGPCommunity extends IQuery {
export interface IQueryBGPRoute extends TQuery {}
export interface IQueryBGPASPath extends TQuery {}
export interface IQueryPing extends TQuery {}
export interface IQueryTraceroute extends TQuery {}
export interface IQueryBGPCommunity extends TQuery {
mode: 'input' | 'select';
communities: IBGPCommunity[];
communities: TBGPCommunity[];
}
export interface IConfigQueries {
export interface TConfigQueries {
bgp_route: IQueryBGPRoute;
bgp_community: IQueryBGPCommunity;
bgp_aspath: IQueryBGPASPath;
ping: IQueryPing;
traceroute: IQueryTraceroute;
list: IQuery[];
list: TQuery[];
}
interface IDeviceVrfBase {
interface TDeviceVrfBase {
id: string;
display_name: string;
}
export interface IDeviceVrf extends IDeviceVrfBase {
export interface TDeviceVrf extends TDeviceVrfBase {
ipv4: boolean;
ipv6: boolean;
}
@ -117,11 +117,11 @@ interface TDeviceBase {
}
export interface TDevice extends TDeviceBase {
vrfs: IDeviceVrf[];
vrfs: TDeviceVrf[];
}
export interface TNetworkLocation extends TDeviceBase {
vrfs: IDeviceVrfBase[];
vrfs: TDeviceVrfBase[];
}
export interface TNetwork {
@ -173,10 +173,23 @@ export interface IConfig {
web: IConfigWeb;
messages: IConfigMessages;
hyperglass_version: string;
queries: IConfigQueries;
queries: TConfigQueries;
devices: TDevice[];
networks: TNetwork[];
vrfs: IDeviceVrfBase[];
vrfs: TDeviceVrfBase[];
parsed_data_fields: TParsedDataField[];
content: IConfigContent;
}
export interface Favicon {
rel: string | null;
dimensions: [number, number];
image_format: string;
prefix: string;
}
export interface FaviconComponent {
rel: string;
href: string;
type: string;
}

View file

@ -1,8 +1,8 @@
export type TQueryTypes = '' | 'bgp_route' | 'bgp_community' | 'bgp_aspath' | 'ping' | 'traceroute';
export interface IFormData {
export interface TFormData {
query_location: string[];
query_type: TQueryTypes | '';
query_type: TQueryTypes;
query_vrf: string;
query_target: string;
}

View file

@ -0,0 +1,204 @@
/**
* DNS Over HTTPS Types, primarily adapted from:
*
* @see https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml
* @see https://developers.cloudflare.com/1.1.1.1/dns-over-https/json-format
* @see https://developers.google.com/speed/public-dns/docs/doh/json
*/
export namespace DnsOverHttps {
/**
* DNS RCODEs
* @see https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6
*/
export enum Status {
/**
* No Error
*/
NO_ERROR = 0,
/**
* Format Error
*/
FORM_ERR = 1,
/**
* Server Failure
*/
SERV_FAIL = 2,
/**
* Non-Existent Domain
*/
NX_DOMAIN = 3,
/**
* Not Implemented
*/
NOT_IMP = 4,
/**
* Query Refused
*/
REFUSED = 5,
/**
* Name Exists when it should not
*/
YX_DOMAIN = 6,
/**
* RR Set Exists when it should not
*/
YXRR_SET = 7,
/**
* RR Set that should exist does not
*/
NXRR_SET = 8,
/**
* Server Not Authoritative for zone
*/
NOT_AUTH = 9,
/**
* Name not contained in zone
*/
NOT_ZONE = 10,
/**
* DSO-TYPE Not Implemented
*/
DSO_TYPE_NI = 11,
/**
* TSIG Signature Failure
*/
BADSIG = 16,
/**
* Key not recognized
*/
BADKEY = 17,
/**
* Signature out of time window
*/
BADTIME = 18,
/**
* Bad TKEY Mode
*/
BADMODE = 19,
/**
* Duplicate key name
*/
BADNAME = 20,
/**
* Algorithm not supported
*/
BADALG = 21,
/**
* Bad Truncation
*/
BADTRUNC = 22,
/**
* Bad/missing Server Cookie
*/
BADCOOKIE = 23,
}
/**
* Resource Record (RR) Types
* @see https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4
*/
export enum Type {
/**
* IPv4 Host Address Record.
*/
A = 1,
/**
* Name Server Record.
*/
NS = 2,
/**
* Canonical Alias Name Record.
*/
CNAME = 5,
/**
* Start of Zone Authority Record.
*/
SOA = 6,
/**
* Well Know Service Description Record.
*/
WKS = 11,
/**
* Domain Name Pointer Record.
*/
PTR = 12,
/**
* Mail Exchange Record.
*/
MX = 15,
/**
* IPv6 Host Address Record.
*/
AAAA = 28,
/**
* Server Selection Record.
*/
SRV = 33,
/**
* DNAME Record.
*/
DNAME = 39,
/**
* DNSKEY Record.
*/
DNSKEY = 48,
}
export interface Question {
/**
* FQDN with trailing dot.
*/
name: string;
/**
* DNS RR Type.
*/
type: Type;
}
export interface Answer {
/**
* FQDN with trailing dot.
*/
name: string;
/**
* DNS RR Type.
*/
type: Type;
/**
* Time to live in seconds.
*/
TTL: number;
/**
* Response data.
*/
data: string;
}
export interface Response {
Status: Status;
/**
* Truncated bit was set.
*/
TC: boolean;
/**
* Recursive Desired bit was set.
*/
RD: boolean;
/**
* Recursion Available bit was set.
*/
RA: boolean;
/**
* If true, it means that every record in the answer was verified with DNSSEC.
*/
AD: boolean;
/**
* If true, the client asked to disable DNSSEC validation.
*/
CD: boolean;
/**
* Queried Resources.
*/
Question: Question[];
/**
* Response Data.
*/
Answer: Answer[];
}
}

View file

@ -1,4 +1,6 @@
export * from './common';
export * from './config';
export * from './data';
export * from './dns-over-https';
export * from './table';
export * from './theme';

View file

@ -32,6 +32,7 @@ interface CustomColors {
tertiary: ColorHues;
dark: ColorHues;
light: ColorHues;
success: ColorHues;
}
type AllColors = CustomColors & ChakraColors;

View file

@ -1,37 +1,17 @@
import { theme as chakraTheme } from '@chakra-ui/core';
import { theme as chakraTheme } from '@chakra-ui/react';
import chroma from 'chroma-js';
const alphaColors = color => ({
900: chroma(color)
.alpha(0.92)
.css(),
800: chroma(color)
.alpha(0.8)
.css(),
700: chroma(color)
.alpha(0.6)
.css(),
600: chroma(color)
.alpha(0.48)
.css(),
500: chroma(color)
.alpha(0.38)
.css(),
400: chroma(color)
.alpha(0.24)
.css(),
300: chroma(color)
.alpha(0.16)
.css(),
200: chroma(color)
.alpha(0.12)
.css(),
100: chroma(color)
.alpha(0.08)
.css(),
50: chroma(color)
.alpha(0.04)
.css(),
900: chroma(color).alpha(0.92).css(),
800: chroma(color).alpha(0.8).css(),
700: chroma(color).alpha(0.6).css(),
600: chroma(color).alpha(0.48).css(),
500: chroma(color).alpha(0.38).css(),
400: chroma(color).alpha(0.24).css(),
300: chroma(color).alpha(0.16).css(),
200: chroma(color).alpha(0.12).css(),
100: chroma(color).alpha(0.08).css(),
50: chroma(color).alpha(0.04).css(),
});
const generateColors = colorInput => {
@ -166,13 +146,10 @@ export const opposingColor = (theme, color) => {
export const googleFontUrl = (fontFamily, weights = [300, 400, 700]) => {
const urlWeights = weights.join(',');
const fontName = fontFamily
.split(/, /)[0]
.trim()
.replace(/'|"/g, '');
const fontName = fontFamily.split(/, /)[0].trim().replace(/'|"/g, '');
const urlFont = fontName.split(/ /).join('+');
const urlBase = `https://fonts.googleapis.com/css?family=${urlFont}:${urlWeights}&display=swap`;
return urlBase;
};
export { theme as defaultTheme } from '@chakra-ui/core';
export { theme as defaultTheme } from '@chakra-ui/react';

2296
hyperglass/ui/yarn.lock vendored

File diff suppressed because it is too large Load diff