mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
continue typescript & chakra v1 migrations [skip ci]
This commit is contained in:
@@ -3,7 +3,9 @@ import { Button, Icon, Tooltip } from '@chakra-ui/react';
|
||||
|
||||
import type { TPathButton } from './types';
|
||||
|
||||
const PathIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/gr').then(i => i.GrNetwork));
|
||||
const PathIcon = dynamic<MeronexIcon>(() =>
|
||||
import('@meronex/icons/bi').then(i => i.BisNetworkChart),
|
||||
);
|
||||
|
||||
export const PathButton = (props: TPathButton) => {
|
||||
const { onOpen } = props;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { forwardRef } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Popover,
|
||||
@@ -18,24 +19,26 @@ import { useMobile } from '~/context';
|
||||
import { useLGState } from '~/hooks';
|
||||
|
||||
import type { IconButtonProps } from '@chakra-ui/react';
|
||||
import type { OnChangeArgs } from '~/types';
|
||||
import type { TSubmitButton, TRSubmitButton } from './types';
|
||||
|
||||
const SubmitIcon = (props: Omit<IconButtonProps, 'aria-label'>) => {
|
||||
const { isLoading } = props;
|
||||
return (
|
||||
<IconButton
|
||||
size="lg"
|
||||
width={16}
|
||||
type="submit"
|
||||
icon={<FiSearch />}
|
||||
title="Submit Query"
|
||||
colorScheme="primary"
|
||||
isLoading={isLoading}
|
||||
aria-label="Submit Query"
|
||||
/>
|
||||
);
|
||||
};
|
||||
const SubmitIcon = forwardRef<HTMLButtonElement, Omit<IconButtonProps, 'aria-label'>>(
|
||||
(props, ref) => {
|
||||
const { isLoading } = props;
|
||||
return (
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size="lg"
|
||||
width={16}
|
||||
type="submit"
|
||||
icon={<FiSearch />}
|
||||
title="Submit Query"
|
||||
colorScheme="primary"
|
||||
isLoading={isLoading}
|
||||
aria-label="Submit Query"
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Mobile Submit Button
|
||||
|
@@ -1,57 +0,0 @@
|
||||
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, false>) => {
|
||||
const { label, data } = props;
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<Text as="span">{label}</Text>
|
||||
<br />
|
||||
<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) && e !== null) {
|
||||
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 }}
|
||||
/>
|
||||
);
|
||||
};
|
@@ -1,4 +1,3 @@
|
||||
export * from './communitySelect';
|
||||
export * from './field';
|
||||
export * from './queryLocation';
|
||||
export * from './queryTarget';
|
||||
|
@@ -1,46 +1,95 @@
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { useColorValue } from '~/context';
|
||||
import { useMemo } from 'react';
|
||||
import { Input, Text } from '@chakra-ui/react';
|
||||
import { components } from 'react-select';
|
||||
import { If, Select } from '~/components';
|
||||
import { useConfig, useColorValue } from '~/context';
|
||||
import { useLGState } from '~/hooks';
|
||||
|
||||
import type { OptionProps } from 'react-select';
|
||||
import type { TBGPCommunity, TSelectOption } from '~/types';
|
||||
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;
|
||||
|
||||
function buildOptions(communities: TBGPCommunity[]): TSelectOption[] {
|
||||
return communities.map(c => ({
|
||||
value: c.community,
|
||||
label: c.display_name,
|
||||
description: c.description,
|
||||
}));
|
||||
}
|
||||
|
||||
const Option = (props: OptionProps<Dict, false>) => {
|
||||
const { label, data } = props;
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<Text as="span">{label}</Text>
|
||||
<br />
|
||||
<Text fontSize="xs" as="span">
|
||||
{data.description}
|
||||
</Text>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
export const QueryTarget = (props: TQueryTarget) => {
|
||||
const { name, register, setTarget, placeholder, resolveTarget } = props;
|
||||
const { name, register, onChange, placeholder, resolveTarget } = props;
|
||||
|
||||
const bg = useColorValue('white', 'whiteAlpha.100');
|
||||
const color = useColorValue('gray.400', 'whiteAlpha.800');
|
||||
const border = useColorValue('gray.100', 'whiteAlpha.50');
|
||||
const placeholderColor = useColorValue('gray.600', 'whiteAlpha.700');
|
||||
|
||||
const { queryTarget, fqdnTarget, displayTarget } = useLGState();
|
||||
const { queryType, queryTarget, fqdnTarget, displayTarget } = useLGState();
|
||||
|
||||
const { queries } = useConfig();
|
||||
|
||||
const options = useMemo(() => buildOptions(queries.bgp_community.communities), []);
|
||||
|
||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>): void {
|
||||
displayTarget.set(e.target.value);
|
||||
setTarget({ field: name, value: e.target.value });
|
||||
onChange({ field: name, value: e.target.value });
|
||||
|
||||
if (resolveTarget && displayTarget.value && fqdnPattern.test(displayTarget.value)) {
|
||||
fqdnTarget.set(displayTarget.value);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectChange(e: TSelectOption | TSelectOption[]): void {
|
||||
if (!Array.isArray(e) && e !== null) {
|
||||
onChange({ field: name, value: e.value });
|
||||
displayTarget.set(e.value);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<input hidden readOnly name={name} ref={register} value={queryTarget.value} />
|
||||
<Input
|
||||
bg={bg}
|
||||
size="lg"
|
||||
color={color}
|
||||
borderRadius="md"
|
||||
value={displayTarget.value}
|
||||
borderColor={border}
|
||||
onChange={handleChange}
|
||||
aria-label={placeholder}
|
||||
placeholder={placeholder}
|
||||
name="query_target_display"
|
||||
_placeholder={{ color: placeholderColor }}
|
||||
/>
|
||||
<If c={queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select'}>
|
||||
<Select
|
||||
size="lg"
|
||||
name={name}
|
||||
options={options}
|
||||
innerRef={register}
|
||||
onChange={handleSelectChange}
|
||||
components={{ Option }}
|
||||
/>
|
||||
</If>
|
||||
<If c={!(queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select')}>
|
||||
<Input
|
||||
bg={bg}
|
||||
size="lg"
|
||||
color={color}
|
||||
borderRadius="md"
|
||||
borderColor={border}
|
||||
onChange={handleChange}
|
||||
aria-label={placeholder}
|
||||
placeholder={placeholder}
|
||||
value={displayTarget.value}
|
||||
name="query_target_display"
|
||||
_placeholder={{ color: placeholderColor }}
|
||||
/>
|
||||
</If>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -29,7 +29,6 @@ export interface TCommunitySelect {
|
||||
onChange: OnChange;
|
||||
communities: TBGPCommunity[];
|
||||
register: Control['register'];
|
||||
unregister: Control['unregister'];
|
||||
}
|
||||
|
||||
export interface TQueryTarget {
|
||||
@@ -37,7 +36,7 @@ export interface TQueryTarget {
|
||||
placeholder: string;
|
||||
resolveTarget: boolean;
|
||||
register: Control['register'];
|
||||
setTarget(e: OnChangeArgs): void;
|
||||
onChange(e: OnChangeArgs): void;
|
||||
}
|
||||
|
||||
export interface TResolvedTarget {
|
||||
|
@@ -31,19 +31,15 @@ export const HelpModal = (props: THelpModal) => {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
initial={{ opacity: 0, scale: 0.3 }}>
|
||||
<IconButton
|
||||
h={3}
|
||||
w={3}
|
||||
mb={1}
|
||||
ml={1}
|
||||
maxH={3}
|
||||
maxW={3}
|
||||
minH={3}
|
||||
minW={3}
|
||||
size="sm"
|
||||
size="md"
|
||||
variant="link"
|
||||
icon={<Info />}
|
||||
onClick={onOpen}
|
||||
colorScheme="primary"
|
||||
colorScheme="blue"
|
||||
aria-label={`${name}_help`}
|
||||
/>
|
||||
</motion.div>
|
||||
|
@@ -15,7 +15,6 @@ import {
|
||||
QueryTarget,
|
||||
SubmitButton,
|
||||
QueryLocation,
|
||||
CommunitySelect,
|
||||
} from '~/components';
|
||||
import { useConfig } from '~/context';
|
||||
import { useStrf, useGreeting, useDevice, useLGState } from '~/hooks';
|
||||
@@ -37,16 +36,17 @@ export const HyperglassForm = () => {
|
||||
|
||||
const formSchema = yup.object().shape({
|
||||
query_location: yup.array().of(yup.string()).required(noQueryLoc),
|
||||
query_target: yup.string().required(noQueryTarget),
|
||||
query_type: yup.string().required(noQueryType),
|
||||
query_vrf: yup.string(),
|
||||
query_target: yup.string().required(noQueryTarget),
|
||||
});
|
||||
|
||||
const formInstance = useForm<TFormData>({
|
||||
resolver: yupResolver(formSchema),
|
||||
defaultValues: { query_vrf: 'default', query_target: '', query_location: [], query_type: '' },
|
||||
});
|
||||
const { handleSubmit, register, unregister, setValue, errors } = formInstance;
|
||||
|
||||
const { handleSubmit, register, unregister, setValue, getValues } = formInstance;
|
||||
|
||||
const {
|
||||
queryVrf,
|
||||
@@ -63,6 +63,8 @@ export const HyperglassForm = () => {
|
||||
} = useLGState();
|
||||
|
||||
function submitHandler(values: TFormData) {
|
||||
console.dir(values);
|
||||
console.dir(formData.value);
|
||||
if (!greetingAck && web.greeting.required) {
|
||||
window.location.reload(false);
|
||||
setGreetingAck(false);
|
||||
@@ -141,6 +143,8 @@ export const HyperglassForm = () => {
|
||||
} else if (e.field === 'query_target' && isString(e.value)) {
|
||||
queryTarget.set(e.value);
|
||||
}
|
||||
console.log(e.field, e.value);
|
||||
console.dir(getValues());
|
||||
}
|
||||
|
||||
const vrfContent = useMemo(() => {
|
||||
@@ -155,9 +159,9 @@ export const HyperglassForm = () => {
|
||||
|
||||
useEffect(() => {
|
||||
register({ name: 'query_location', required: true });
|
||||
register({ name: 'query_target', required: true });
|
||||
register({ name: 'query_type', required: true });
|
||||
register({ name: 'query_vrf' });
|
||||
register({ name: 'query_target', required: true });
|
||||
}, [register]);
|
||||
|
||||
return (
|
||||
@@ -192,25 +196,13 @@ export const HyperglassForm = () => {
|
||||
</FormField>
|
||||
</If>
|
||||
<FormField name="query_target" label={web.text.query_target}>
|
||||
<If c={queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select'}>
|
||||
<CommunitySelect
|
||||
name="query_target"
|
||||
register={register}
|
||||
unregister={unregister}
|
||||
onChange={handleChange}
|
||||
communities={queries.bgp_community.communities}
|
||||
/>
|
||||
</If>
|
||||
<If
|
||||
c={!(queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select')}>
|
||||
<QueryTarget
|
||||
name="query_target"
|
||||
register={register}
|
||||
setTarget={handleChange}
|
||||
resolveTarget={isFqdnQuery}
|
||||
placeholder={web.text.query_target}
|
||||
/>
|
||||
</If>
|
||||
<QueryTarget
|
||||
name="query_target"
|
||||
register={register}
|
||||
onChange={handleChange}
|
||||
resolveTarget={isFqdnQuery}
|
||||
placeholder={web.text.query_target}
|
||||
/>
|
||||
</FormField>
|
||||
</FormRow>
|
||||
<FormRow mt={0} justifyContent="flex-end">
|
||||
|
@@ -1,40 +1,52 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Flex, Icon, Badge, VStack } from '@chakra-ui/react';
|
||||
import { BeatLoader } from 'react-spinners';
|
||||
import { Box, Flex, SkeletonText, Badge, VStack } from '@chakra-ui/react';
|
||||
import ReactFlow from 'react-flow-renderer';
|
||||
import { Background, Controls } from 'react-flow-renderer';
|
||||
import { Background, ReactFlowProvider } from 'react-flow-renderer';
|
||||
import { Handle, Position } from 'react-flow-renderer';
|
||||
import { useConfig, useColorValue, useColorToken } from '~/context';
|
||||
import { useConfig, useColorValue, useColorToken, useBreakpointValue } from '~/context';
|
||||
import { useASNDetail } from '~/hooks';
|
||||
import { Controls } from './controls';
|
||||
import { buildElements } from './util';
|
||||
|
||||
import type { ReactFlowProps } from 'react-flow-renderer';
|
||||
import type { TChart, TNode, TNodeData } from './types';
|
||||
|
||||
export const Chart = (props: TChart) => {
|
||||
const { data } = props;
|
||||
const { primary_asn, org_name } = useConfig();
|
||||
// const elements = useMemo(() => [...buildElements({ asn: primary_asn, name: org_name }, data)], [
|
||||
// data,
|
||||
// ]);
|
||||
|
||||
const elements = [...buildElements({ asn: primary_asn, name: org_name }, data)];
|
||||
const dots = useColorToken('blackAlpha.500', 'whiteAlpha.400');
|
||||
|
||||
const flowProps = useBreakpointValue<Omit<ReactFlowProps, 'elements'>>({
|
||||
base: { defaultPosition: [0, 300], defaultZoom: 0 },
|
||||
lg: { defaultPosition: [500, 450] },
|
||||
});
|
||||
|
||||
const elements = useMemo(() => [...buildElements({ asn: primary_asn, name: org_name }, data)], [
|
||||
data,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Box boxSize="100%">
|
||||
<ReactFlow elements={elements} nodeTypes={{ TestNode }} defaultPosition={[500, 450]}>
|
||||
<Background color={dots} />
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
</Box>
|
||||
<ReactFlowProvider>
|
||||
<Box boxSize="100%" zIndex={1}>
|
||||
<ReactFlow elements={elements} nodeTypes={{ TestNode }} {...flowProps}>
|
||||
<Background color={dots} />
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
</Box>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const TestNode = (props: TNode<TNodeData>) => {
|
||||
const { data } = props;
|
||||
const { asn, name, hasChildren, hasParents } = data;
|
||||
|
||||
const color = useColorValue('black', 'white');
|
||||
const bg = useColorValue('white', 'blackAlpha.800');
|
||||
const bg = useColorValue('white', 'whiteAlpha.100');
|
||||
|
||||
const { data: asnData, isError, isLoading } = useASNDetail(asn);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasChildren && <Handle type="source" position={Position.Top} />}
|
||||
@@ -42,7 +54,9 @@ const TestNode = (props: TNode<TNodeData>) => {
|
||||
<VStack spacing={4}>
|
||||
<Flex fontSize="lg">
|
||||
{isLoading ? (
|
||||
<BeatLoader color={color} />
|
||||
<Box h={2} w={24}>
|
||||
<SkeletonText noOfLines={1} color={color} />
|
||||
</Box>
|
||||
) : !isError && asnData?.data?.description_short ? (
|
||||
asnData.data.description_short
|
||||
) : (
|
||||
|
27
hyperglass/ui/components/path/controls.tsx
Normal file
27
hyperglass/ui/components/path/controls.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ButtonGroup, IconButton } from '@chakra-ui/react';
|
||||
import { useZoomPanHelper } from 'react-flow-renderer';
|
||||
|
||||
const Plus = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiPlus));
|
||||
const Minus = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiMinus));
|
||||
const Square = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiSquare));
|
||||
|
||||
export const Controls = () => {
|
||||
const { fitView, zoomIn, zoomOut } = useZoomPanHelper();
|
||||
return (
|
||||
<ButtonGroup
|
||||
m={4}
|
||||
size="sm"
|
||||
right={0}
|
||||
zIndex={4}
|
||||
bottom={0}
|
||||
isAttached
|
||||
pos="absolute"
|
||||
variant="solid"
|
||||
colorScheme="secondary">
|
||||
<IconButton icon={<Plus />} onClick={() => zoomIn()} aria-label="Zoom In" />
|
||||
<IconButton icon={<Minus />} onClick={() => zoomOut()} aria-label="Zoom Out" />
|
||||
<IconButton icon={<Square />} onClick={() => fitView()} aria-label="Fit Nodes" />
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
@@ -22,13 +22,13 @@ export const Path = (props: TPath) => {
|
||||
const { displayTarget } = useLGState();
|
||||
const response = getResponse(device);
|
||||
const output = response?.output as TStructuredResponse;
|
||||
const bg = useColorValue('white', 'blackFaded.800');
|
||||
const bg = useColorValue('whiteFaded.50', 'blackFaded.900');
|
||||
return (
|
||||
<>
|
||||
<PathButton onOpen={onOpen} />
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="full" isCentered>
|
||||
<ModalOverlay />
|
||||
<ModalContent bg={bg} maxW="80%" maxH="60%">
|
||||
<ModalContent bg={bg} maxW={{ base: '100%', lg: '80%' }} maxH={{ base: '80%', lg: '60%' }}>
|
||||
<ModalHeader>{`Path to ${displayTarget.value}`}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
|
@@ -148,6 +148,7 @@ export const Results = () => {
|
||||
const device = getDevice(loc.value);
|
||||
return (
|
||||
<Result
|
||||
key={i}
|
||||
index={i}
|
||||
device={device}
|
||||
queryLocation={loc.value}
|
||||
|
Reference in New Issue
Block a user