mirror of
https://github.com/thatmattlove/hyperglass.git
synced 2026-01-17 08:48:05 +00:00
improve global state management & data flow [skip ci]
This commit is contained in:
parent
57f2a2f807
commit
8144ae4c1e
21 changed files with 335 additions and 164 deletions
|
|
@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
|||
import { useFormContext } from 'react-hook-form';
|
||||
import { Select } from '~/components';
|
||||
import { useConfig } from '~/context';
|
||||
import { useLGState, useLGMethods } from '~/hooks';
|
||||
|
||||
import type { TNetwork, TSelectOption } from '~/types';
|
||||
import type { TQuerySelectField } from './types';
|
||||
|
|
@ -23,6 +24,8 @@ export const QueryLocation = (props: TQuerySelectField) => {
|
|||
|
||||
const { networks } = useConfig();
|
||||
const { errors } = useFormContext();
|
||||
const { selections } = useLGState();
|
||||
const { exportState } = useLGMethods();
|
||||
|
||||
const options = useMemo(() => buildOptions(networks), [networks.length]);
|
||||
|
||||
|
|
@ -35,6 +38,7 @@ export const QueryLocation = (props: TQuerySelectField) => {
|
|||
if (Array.isArray(e)) {
|
||||
const value = e.map(sel => sel!.value);
|
||||
onChange({ field: 'query_location', value });
|
||||
selections.queryLocation.set(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,6 +51,7 @@ export const QueryLocation = (props: TQuerySelectField) => {
|
|||
name="query_location"
|
||||
onChange={handleChange}
|
||||
closeMenuOnSelect={false}
|
||||
value={exportState(selections.queryLocation.value)}
|
||||
isError={typeof errors.query_location !== 'undefined'}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -33,26 +33,22 @@ const Option = (props: OptionProps<Dict, false>) => {
|
|||
};
|
||||
|
||||
export const QueryTarget = (props: TQueryTarget) => {
|
||||
const { name, register, onChange, placeholder, resolveTarget } = props;
|
||||
const { name, register, onChange, placeholder } = 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 { queryType, queryTarget, fqdnTarget, displayTarget } = useLGState();
|
||||
const { queryType, queryTarget, displayTarget } = useLGState();
|
||||
|
||||
const { queries } = useConfig();
|
||||
|
||||
const options = useMemo(() => buildOptions(queries.bgp_community.communities), []);
|
||||
|
||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>): void {
|
||||
function handleInputChange(e: React.ChangeEvent<HTMLInputElement>): void {
|
||||
displayTarget.set(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 {
|
||||
|
|
@ -71,8 +67,8 @@ export const QueryTarget = (props: TQueryTarget) => {
|
|||
name={name}
|
||||
options={options}
|
||||
innerRef={register}
|
||||
onChange={handleSelectChange}
|
||||
components={{ Option }}
|
||||
onChange={handleSelectChange}
|
||||
/>
|
||||
</If>
|
||||
<If c={!(queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select')}>
|
||||
|
|
@ -82,11 +78,11 @@ export const QueryTarget = (props: TQueryTarget) => {
|
|||
color={color}
|
||||
borderRadius="md"
|
||||
borderColor={border}
|
||||
onChange={handleChange}
|
||||
aria-label={placeholder}
|
||||
placeholder={placeholder}
|
||||
value={displayTarget.value}
|
||||
name="query_target_display"
|
||||
onChange={handleInputChange}
|
||||
_placeholder={{ color: placeholderColor }}
|
||||
/>
|
||||
</If>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
|||
import { useFormContext } from 'react-hook-form';
|
||||
import { Select } from '~/components';
|
||||
import { useConfig } from '~/context';
|
||||
import { useLGState, useLGMethods } from '~/hooks';
|
||||
|
||||
import type { TQuery, TSelectOption } from '~/types';
|
||||
import type { TQuerySelectField } from './types';
|
||||
|
|
@ -16,12 +17,17 @@ export const QueryType = (props: TQuerySelectField) => {
|
|||
const { onChange, label } = props;
|
||||
const { queries } = useConfig();
|
||||
const { errors } = useFormContext();
|
||||
const { selections } = useLGState();
|
||||
const { exportState } = useLGMethods();
|
||||
|
||||
const options = useMemo(() => buildOptions(queries.list), [queries.list.length]);
|
||||
|
||||
function handleChange(e: TSelectOption | TSelectOption[]): void {
|
||||
if (!Array.isArray(e) && e !== null) {
|
||||
selections.queryType.set(e);
|
||||
onChange({ field: 'query_type', value: e.value });
|
||||
} else {
|
||||
selections.queryType.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +38,7 @@ export const QueryType = (props: TQuerySelectField) => {
|
|||
options={options}
|
||||
aria-label={label}
|
||||
onChange={handleChange}
|
||||
value={exportState(selections.queryType.value)}
|
||||
isError={typeof errors.query_type !== 'undefined'}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Select } from '~/components';
|
||||
import { useLGMethods, useLGState } from '~/hooks';
|
||||
|
||||
import { TDeviceVrf, TSelectOption } from '~/types';
|
||||
import type { TQueryVrf } from './types';
|
||||
|
|
@ -10,12 +11,17 @@ function buildOptions(queryVrfs: TDeviceVrf[]): TSelectOption[] {
|
|||
|
||||
export const QueryVrf = (props: TQueryVrf) => {
|
||||
const { vrfs, onChange, label } = props;
|
||||
const { selections } = useLGState();
|
||||
const { exportState } = useLGMethods();
|
||||
|
||||
const options = useMemo(() => buildOptions(vrfs), [vrfs.length]);
|
||||
|
||||
function handleChange(e: TSelectOption | TSelectOption[]): void {
|
||||
if (!Array.isArray(e) && e !== null) {
|
||||
selections.queryVrf.set(e);
|
||||
onChange({ field: 'query_vrf', value: e.value });
|
||||
} else {
|
||||
selections.queryVrf.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -26,6 +32,7 @@ export const QueryVrf = (props: TQueryVrf) => {
|
|||
options={options}
|
||||
aria-label={label}
|
||||
onChange={handleChange}
|
||||
value={exportState(selections.queryVrf.value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,11 +19,10 @@ function findAnswer(data: DnsOverHttps.Response | undefined): string {
|
|||
export const ResolvedTarget = (props: TResolvedTarget) => {
|
||||
const { setTarget } = props;
|
||||
const { web } = useConfig();
|
||||
const { fqdnTarget, isSubmitting, families, formData } = useLGState();
|
||||
const { displayTarget, isSubmitting, families, queryTarget } = useLGState();
|
||||
|
||||
const color = useColorValue('secondary.500', 'secondary.300');
|
||||
|
||||
const dnsUrl = web.dns_provider.url;
|
||||
const query4 = Array.from(families.value).includes(4);
|
||||
const query6 = Array.from(families.value).includes(6);
|
||||
|
||||
|
|
@ -34,12 +33,12 @@ export const ResolvedTarget = (props: TResolvedTarget) => {
|
|||
]);
|
||||
|
||||
const { data: data4, isLoading: isLoading4, isError: isError4 } = useDNSQuery(
|
||||
fqdnTarget.value,
|
||||
displayTarget.value,
|
||||
4,
|
||||
);
|
||||
|
||||
const { data: data6, isLoading: isLoading6, isError: isError6 } = useDNSQuery(
|
||||
fqdnTarget.value,
|
||||
displayTarget.value,
|
||||
6,
|
||||
);
|
||||
|
||||
|
|
@ -47,7 +46,7 @@ export const ResolvedTarget = (props: TResolvedTarget) => {
|
|||
setTarget({ field: 'query_target', value });
|
||||
}
|
||||
function selectTarget(value: string): void {
|
||||
formData.set(p => ({ ...p, query_target: value }));
|
||||
queryTarget.set(value);
|
||||
isSubmitting.set(true);
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +65,7 @@ export const ResolvedTarget = (props: TResolvedTarget) => {
|
|||
<Text fontSize="sm" textAlign="center">
|
||||
{messageStart}
|
||||
<Text as="span" fontSize="sm" fontWeight="bold" color={color}>
|
||||
{`${fqdnTarget.value}`.toLowerCase()}
|
||||
{`${displayTarget.value}`.toLowerCase()}
|
||||
</Text>
|
||||
{messageEnd}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { FormControlProps } from '@chakra-ui/react';
|
||||
import type { FieldError, Control } from 'react-hook-form';
|
||||
import type { Control } from 'react-hook-form';
|
||||
import type { TDeviceVrf, TBGPCommunity, OnChangeArgs } from '~/types';
|
||||
import type { ValidationError } from 'yup';
|
||||
|
||||
|
|
@ -34,7 +34,6 @@ export interface TCommunitySelect {
|
|||
export interface TQueryTarget {
|
||||
name: string;
|
||||
placeholder: string;
|
||||
resolveTarget: boolean;
|
||||
register: Control['register'];
|
||||
onChange(e: OnChangeArgs): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Flex, Button, VStack } from '@chakra-ui/react';
|
|||
import { motion } from 'framer-motion';
|
||||
import { If } from '~/components';
|
||||
import { useConfig, useMobile } from '~/context';
|
||||
import { useBooleanValue, useLGState } from '~/hooks';
|
||||
import { useBooleanValue, useLGState, useLGMethods } from '~/hooks';
|
||||
import { Logo } from './logo';
|
||||
import { TitleOnly } from './titleOnly';
|
||||
import { SubtitleOnly } from './subtitleOnly';
|
||||
|
|
@ -98,7 +98,8 @@ export const Title = (props: TTitle) => {
|
|||
const { web } = useConfig();
|
||||
const titleMode = web.text.title_mode;
|
||||
|
||||
const { isSubmitting, resetForm } = useLGState();
|
||||
const { isSubmitting } = useLGState();
|
||||
const { resetForm } = useLGMethods();
|
||||
|
||||
const titleHeight = useBooleanValue(isSubmitting.value, undefined, { md: '20vh' });
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ import { useRef } from 'react';
|
|||
import { Flex } from '@chakra-ui/react';
|
||||
import { If, Debugger, Greeting, Footer, Header } from '~/components';
|
||||
import { useConfig, useColorValue } from '~/context';
|
||||
import { useLGState } from '~/hooks';
|
||||
import { useLGState, useLGMethods } from '~/hooks';
|
||||
import { ResetButton } from './resetButton';
|
||||
|
||||
import type { TFrame } from './types';
|
||||
|
||||
export const Frame = (props: TFrame) => {
|
||||
const { developer_mode } = useConfig();
|
||||
const { isSubmitting, resetForm } = useLGState();
|
||||
const { isSubmitting } = useLGState();
|
||||
const { resetForm } = useLGMethods();
|
||||
|
||||
const bg = useColorValue('white', 'black');
|
||||
const color = useColorValue('black', 'white');
|
||||
|
|
|
|||
|
|
@ -1,30 +1,19 @@
|
|||
import { AnimatePresence } from 'framer-motion';
|
||||
import { If, HyperglassForm, Results } from '~/components';
|
||||
import { useLGState } from '~/hooks';
|
||||
import { all } from '~/util';
|
||||
import { LookingGlass, Results } from '~/components';
|
||||
import { useLGMethods } from '~/hooks';
|
||||
import { Frame } from './frame';
|
||||
|
||||
export const Layout: React.FC = () => {
|
||||
const { isSubmitting, formData } = useLGState();
|
||||
const { formReady } = useLGMethods();
|
||||
return (
|
||||
<Frame>
|
||||
<If
|
||||
c={
|
||||
isSubmitting.value &&
|
||||
all(
|
||||
formData.query_location.value,
|
||||
formData.query_target.value,
|
||||
formData.query_type.value,
|
||||
formData.query_vrf.value,
|
||||
)
|
||||
}>
|
||||
{formReady() ? (
|
||||
<Results />
|
||||
</If>
|
||||
<AnimatePresence>
|
||||
<If c={!isSubmitting.value}>
|
||||
<HyperglassForm />
|
||||
</If>
|
||||
</AnimatePresence>
|
||||
) : (
|
||||
<AnimatePresence>
|
||||
<LookingGlass />
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</Frame>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { intersectionWith } from 'lodash';
|
||||
|
|
@ -17,17 +17,34 @@ import {
|
|||
QueryLocation,
|
||||
} from '~/components';
|
||||
import { useConfig } from '~/context';
|
||||
import { useStrf, useGreeting, useDevice, useLGState } from '~/hooks';
|
||||
import { useStrf, useGreeting, useDevice, useLGState, useLGMethods } from '~/hooks';
|
||||
import { isQueryType, isQueryContent, isString } from '~/types';
|
||||
|
||||
import type { TFormData, TDeviceVrf, OnChangeArgs } from '~/types';
|
||||
|
||||
const fqdnPattern = /^(?!:\/\/)([a-zA-Z0-9-]+\.)?[a-zA-Z0-9-][a-zA-Z0-9-]+\.[a-zA-Z-]{2,6}?$/gim;
|
||||
/**
|
||||
* Don't set the global flag on this.
|
||||
* @see https://stackoverflow.com/questions/24084926/javascript-regexp-cant-use-twice
|
||||
*
|
||||
* TLDR: the test() will pass the first time, but not the second. In React Strict Mode & in a dev
|
||||
* environment, this will mean isFqdn will be true the first time, then false the second time,
|
||||
* submitting the FQDN to hyperglass the second time.
|
||||
*/
|
||||
const fqdnPattern = new RegExp(
|
||||
/^(?!:\/\/)([a-zA-Z0-9-]+\.)?[a-zA-Z0-9-][a-zA-Z0-9-]+\.[a-zA-Z-]{2,6}?$/im,
|
||||
);
|
||||
|
||||
export const HyperglassForm = () => {
|
||||
const { web, content, messages, queries } = useConfig();
|
||||
function useIsFqdn(target: string, _type: string) {
|
||||
return useCallback(
|
||||
(): boolean => ['bgp_route', 'ping', 'traceroute'].includes(_type) && fqdnPattern.test(target),
|
||||
[target, _type],
|
||||
);
|
||||
}
|
||||
|
||||
const [greetingAck, setGreetingAck] = useGreeting();
|
||||
export const LookingGlass = () => {
|
||||
const { web, content, messages } = useConfig();
|
||||
|
||||
const { ack, greetingReady, isOpen: greetingIsOpen } = useGreeting();
|
||||
const getDevice = useDevice();
|
||||
|
||||
const noQueryType = useStrf(messages.no_input, { field: web.text.query_type });
|
||||
|
|
@ -46,34 +63,55 @@ export const HyperglassForm = () => {
|
|||
defaultValues: { query_vrf: 'default', query_target: '', query_location: [], query_type: '' },
|
||||
});
|
||||
|
||||
const { handleSubmit, register, unregister, setValue, getValues } = formInstance;
|
||||
const { handleSubmit, register, setValue } = formInstance;
|
||||
|
||||
const {
|
||||
queryVrf,
|
||||
families,
|
||||
formData,
|
||||
queryType,
|
||||
availVrfs,
|
||||
fqdnTarget,
|
||||
btnLoading,
|
||||
queryTarget,
|
||||
isSubmitting,
|
||||
resolvedOpen,
|
||||
queryLocation,
|
||||
displayTarget,
|
||||
} = useLGState();
|
||||
|
||||
function submitHandler(values: TFormData) {
|
||||
if (!greetingAck && web.greeting.required) {
|
||||
window.location.reload(false);
|
||||
setGreetingAck(false);
|
||||
} else if (fqdnPattern.test(values.query_target)) {
|
||||
const { resolvedOpen, resetForm } = useLGMethods();
|
||||
|
||||
const isFqdnQuery = useIsFqdn(queryTarget.value, queryType.value);
|
||||
|
||||
function submitHandler() {
|
||||
/**
|
||||
* Before submitting a query, make sure the greeting is acknowledged if required. This should
|
||||
* be handled before loading the app, but people be sneaky.
|
||||
*/
|
||||
if (!greetingReady()) {
|
||||
resetForm();
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// Determine if queryTarget is an FQDN.
|
||||
const isFqdn = isFqdnQuery();
|
||||
|
||||
if (greetingReady() && !isFqdn) {
|
||||
return isSubmitting.set(true);
|
||||
}
|
||||
|
||||
if (greetingReady() && isFqdn) {
|
||||
btnLoading.set(true);
|
||||
fqdnTarget.set(values.query_target);
|
||||
formData.set(values);
|
||||
resolvedOpen();
|
||||
return resolvedOpen();
|
||||
} else {
|
||||
formData.set(values);
|
||||
isSubmitting.set(true);
|
||||
console.group('%cSomething went wrong', 'color:red;');
|
||||
console.table({
|
||||
'Greeting Required': web.greeting.required,
|
||||
'Greeting Ready': greetingReady(),
|
||||
'Greeting Acknowledged': ack.value,
|
||||
'Query Target': queryTarget.value,
|
||||
'Query Type': queryType.value,
|
||||
'Is FQDN': isFqdn,
|
||||
});
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,38 +142,65 @@ export const HyperglassForm = () => {
|
|||
queryVrf.set('default');
|
||||
}
|
||||
|
||||
// Determine which address families are available in the intersecting VRFs.
|
||||
let ipv4 = 0;
|
||||
let ipv6 = 0;
|
||||
|
||||
if (intersecting.length !== 0) {
|
||||
for (const intersection of intersecting) {
|
||||
if (intersection.ipv4) {
|
||||
ipv4++;
|
||||
}
|
||||
if (intersection.ipv6) {
|
||||
ipv6++;
|
||||
}
|
||||
for (const intersection of intersecting) {
|
||||
if (intersection.ipv4) {
|
||||
// If IPv4 is enabled in this VRF, count it.
|
||||
ipv4++;
|
||||
}
|
||||
if (intersection.ipv6) {
|
||||
// If IPv6 is enabled in this VRF, count it.
|
||||
ipv6++;
|
||||
}
|
||||
}
|
||||
|
||||
if (ipv4 !== 0 && ipv4 === ipv6) {
|
||||
/**
|
||||
* If ipv4 & ipv6 are equal, this means every VRF has both IPv4 & IPv6 enabled. In that
|
||||
* case, signal that both A & AAAA records should be queried if the query is an FQDN.
|
||||
*/
|
||||
families.set([4, 6]);
|
||||
} else if (ipv4 > ipv6) {
|
||||
/**
|
||||
* If ipv4 is greater than ipv6, this means that IPv6 is not enabled on all VRFs, i.e. there
|
||||
* are some VRFs with IPv4 enabled but IPv6 disabled. In that case, only query A records.
|
||||
*/
|
||||
families.set([4]);
|
||||
} else if (ipv4 < ipv6) {
|
||||
/**
|
||||
* If ipv6 is greater than ipv4, this means that IPv4 is not enabled on all VRFs, i.e. there
|
||||
* are some VRFs with IPv6 enabled but IPv4 disabled. In that case, only query AAAA records.
|
||||
*/
|
||||
families.set([6]);
|
||||
} else {
|
||||
/**
|
||||
* If both ipv4 and ipv6 are 0, then both ipv4 and ipv6 are disabled, and why does that VRF
|
||||
* even exist?
|
||||
*/
|
||||
families.set([]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(e: OnChangeArgs): void {
|
||||
// Signal the field & value to react-hook-form.
|
||||
setValue(e.field, e.value);
|
||||
|
||||
if (e.field === 'query_location' && Array.isArray(e.value)) {
|
||||
handleLocChange(e.value);
|
||||
} else if (e.field === 'query_type' && isQueryType(e.value)) {
|
||||
queryType.set(e.value);
|
||||
if (queryTarget.value !== '') {
|
||||
/**
|
||||
* Reset queryTarget as well, so that, for example, selecting BGP Community, and selecting
|
||||
* a community, then changing the queryType to BGP Route doesn't preserve the selected
|
||||
* community as the queryTarget.
|
||||
*/
|
||||
queryTarget.set('');
|
||||
displayTarget.set('');
|
||||
}
|
||||
} else if (e.field === 'query_vrf' && isString(e.value)) {
|
||||
queryVrf.set(e.value);
|
||||
} else if (e.field === 'query_target' && isString(e.value)) {
|
||||
|
|
@ -143,7 +208,14 @@ export const HyperglassForm = () => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the correct help content based on the selected VRF & Query Type. Also remove the icon
|
||||
* if no locations are set.
|
||||
*/
|
||||
const vrfContent = useMemo(() => {
|
||||
if (queryLocation.value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (Object.keys(content.vrf).includes(queryVrf.value) && queryType.value !== '') {
|
||||
return content.vrf[queryVrf.value][queryType.value];
|
||||
} else {
|
||||
|
|
@ -151,10 +223,6 @@ export const HyperglassForm = () => {
|
|||
}
|
||||
}, [queryVrf.value, queryLocation.value, queryType.value]);
|
||||
|
||||
const isFqdnQuery = useMemo(() => {
|
||||
return ['bgp_route', 'ping', 'traceroute'].includes(queryType.value);
|
||||
}, [queryType.value]);
|
||||
|
||||
useEffect(() => {
|
||||
register({ name: 'query_location', required: true });
|
||||
register({ name: 'query_target', required: true });
|
||||
|
|
@ -200,7 +268,6 @@ export const HyperglassForm = () => {
|
|||
name="query_target"
|
||||
register={register}
|
||||
onChange={handleChange}
|
||||
resolveTarget={isFqdnQuery}
|
||||
placeholder={web.text.query_target}
|
||||
/>
|
||||
</FormField>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
ModalCloseButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { useColorValue } from '~/context';
|
||||
import { useLGState } from '~/hooks';
|
||||
import { useLGState, useLGMethods } from '~/hooks';
|
||||
import { PathButton } from './button';
|
||||
import { Chart } from './chart';
|
||||
|
||||
|
|
@ -17,9 +17,9 @@ import type { TPath } from './types';
|
|||
|
||||
export const Path = (props: TPath) => {
|
||||
const { device } = props;
|
||||
const { getResponse } = useLGState();
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
const { displayTarget } = useLGState();
|
||||
const { getResponse } = useLGMethods();
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
const response = getResponse(device);
|
||||
const output = response?.output as TStructuredResponse;
|
||||
const bg = useColorValue('whiteFaded.50', 'blackFaded.900');
|
||||
|
|
|
|||
|
|
@ -9,13 +9,8 @@ import { Result } from './individual';
|
|||
|
||||
export const Results = () => {
|
||||
const { queries, vrfs, web } = useConfig();
|
||||
const { formData } = useLGState();
|
||||
const {
|
||||
query_location: queryLocation,
|
||||
query_target: queryTarget,
|
||||
query_type: queryType,
|
||||
query_vrf: queryVrf,
|
||||
} = formData;
|
||||
const { queryLocation, queryTarget, queryType, queryVrf } = useLGState();
|
||||
|
||||
const getDevice = useDevice();
|
||||
const targetBg = useToken('colors', 'teal.600');
|
||||
const queryBg = useToken('colors', 'cyan.500');
|
||||
|
|
@ -91,7 +86,7 @@ export const Results = () => {
|
|||
maxW={{ base: '100%', lg: '75%', xl: '50%' }}>
|
||||
<Stack isInline align="center" justify="center" mt={4} flexWrap="wrap">
|
||||
<AnimatePresence>
|
||||
{queryLocation && (
|
||||
{queryLocation.value && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={initialLeft}
|
||||
|
|
@ -150,7 +145,7 @@ export const Results = () => {
|
|||
maxW={{ base: '100%', md: '75%' }}>
|
||||
<Accordion allowMultiple allowToggle index={resultsComplete}>
|
||||
<AnimatePresence>
|
||||
{queryLocation &&
|
||||
{queryLocation.value &&
|
||||
queryLocation.map((loc, i) => {
|
||||
const device = getDevice(loc.value);
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@ export function isFetchError(error: any): error is Response {
|
|||
}
|
||||
|
||||
export function isLGError(error: any): error is TQueryResponse {
|
||||
return error !== null && 'output' in error;
|
||||
return typeof error !== 'undefined' && error !== null && 'output' in error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,11 +93,11 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
|
|||
|
||||
const errorKeywords = useMemo(() => {
|
||||
let kw = [] as string[];
|
||||
if (isLGError(error)) {
|
||||
kw = error.keywords;
|
||||
if (isLGError(data)) {
|
||||
kw = data.keywords;
|
||||
}
|
||||
return kw;
|
||||
}, [isError]);
|
||||
}, [data]);
|
||||
|
||||
let errorMsg;
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
|
|||
errorMsg = messages.general;
|
||||
}
|
||||
|
||||
error && console.error(error);
|
||||
isError && console.error(error);
|
||||
|
||||
const errorLevel = useMemo<TErrorLevels>(() => {
|
||||
const statusMap = {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ButtonProps, FlexProps } from '@chakra-ui/react';
|
||||
import type { QueryResultBase } from 'react-query';
|
||||
import type { UseQueryResult } from 'react-query';
|
||||
import type { TDevice, TQueryTypes } from '~/types';
|
||||
|
||||
export interface TResultHeader {
|
||||
|
|
@ -38,5 +38,5 @@ export interface TCopyButton extends ButtonProps {
|
|||
}
|
||||
|
||||
export interface TRequeryButton extends ButtonProps {
|
||||
requery: QueryResultBase<TQueryResponse>['refetch'];
|
||||
requery: UseQueryResult<TQueryResponse>['refetch'];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,16 +14,17 @@ import {
|
|||
PopoverCloseButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiSearch } from '@meronex/icons/fi';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { If, ResolvedTarget } from '~/components';
|
||||
import { useMobile } from '~/context';
|
||||
import { useLGState } from '~/hooks';
|
||||
import { useLGState, useLGMethods } from '~/hooks';
|
||||
|
||||
import type { IconButtonProps } from '@chakra-ui/react';
|
||||
import type { TSubmitButton, TRSubmitButton } from './types';
|
||||
|
||||
const SubmitIcon = forwardRef<HTMLButtonElement, Omit<IconButtonProps, 'aria-label'>>(
|
||||
(props, ref) => {
|
||||
const { isLoading } = props;
|
||||
const { isLoading, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
ref={ref}
|
||||
|
|
@ -35,6 +36,7 @@ const SubmitIcon = forwardRef<HTMLButtonElement, Omit<IconButtonProps, 'aria-lab
|
|||
colorScheme="primary"
|
||||
isLoading={isLoading}
|
||||
aria-label="Submit Query"
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
@ -87,12 +89,14 @@ const DSubmitButton = (props: TRSubmitButton) => {
|
|||
|
||||
export const SubmitButton = (props: TSubmitButton) => {
|
||||
const { handleChange } = props;
|
||||
const { btnLoading, resolvedIsOpen, resolvedClose, resetForm, isSubmitting } = useLGState();
|
||||
const isMobile = useMobile();
|
||||
const { resolvedIsOpen, btnLoading } = useLGState();
|
||||
const { resolvedClose, resetForm } = useLGMethods();
|
||||
|
||||
const { reset } = useFormContext();
|
||||
|
||||
function handleClose(): void {
|
||||
btnLoading.set(false);
|
||||
isSubmitting.set(false);
|
||||
reset();
|
||||
resetForm();
|
||||
resolvedClose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
import { createState, useState } from '@hookstate/core';
|
||||
import type { TGlobalState, TUseGlobalState } from './types';
|
||||
|
||||
const defaultFormData = {
|
||||
query_location: [],
|
||||
query_target: '',
|
||||
query_type: '',
|
||||
query_vrf: '',
|
||||
} as TGlobalState['formData'];
|
||||
|
||||
const globalState = createState<TGlobalState>({
|
||||
isSubmitting: false,
|
||||
formData: defaultFormData,
|
||||
});
|
||||
|
||||
export function useGlobalState(): TUseGlobalState {
|
||||
const state = useState<TGlobalState>(globalState);
|
||||
function resetForm(): void {
|
||||
state.formData.set(defaultFormData);
|
||||
state.isSubmitting.set(false);
|
||||
}
|
||||
return { resetForm, ...state };
|
||||
}
|
||||
|
|
@ -1,2 +1 @@
|
|||
export * from './HyperglassProvider';
|
||||
export * from './GlobalState';
|
||||
|
|
|
|||
|
|
@ -15,4 +15,10 @@ interface TGlobalStateFunctions {
|
|||
resetForm(): void;
|
||||
}
|
||||
|
||||
export type TUseGlobalState = State<TGlobalState> & TGlobalStateFunctions;
|
||||
// export type TUseGlobalState = State<TGlobalState> & TGlobalStateFunctions;
|
||||
|
||||
export interface TUseGlobalState {
|
||||
isSubmitting: State<TGlobalState['isSubmitting']>;
|
||||
formData: State<TGlobalState['formData']>;
|
||||
resetForm(): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,135 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useState, createState } from '@hookstate/core';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { all } from '~/util';
|
||||
|
||||
import type { State } from '@hookstate/core';
|
||||
import type { Families, TDeviceVrf, TQueryTypes, TFormData } from '~/types';
|
||||
import type { State, PluginStateControl, Plugin } from '@hookstate/core';
|
||||
import type { Families, TDeviceVrf, TQueryTypes, TSelectOption } from '~/types';
|
||||
|
||||
const PluginID = Symbol('Methods');
|
||||
|
||||
/**
|
||||
* Public API
|
||||
*/
|
||||
interface MethodsExtension {
|
||||
getResponse(d: string): TQueryResponse | null;
|
||||
resolvedClose(): void;
|
||||
resolvedOpen(): void;
|
||||
formReady(): boolean;
|
||||
resetForm(): void;
|
||||
}
|
||||
|
||||
class MethodsInstance {
|
||||
public resolvedOpen(state: State<TLGState>) {
|
||||
state.resolvedIsOpen.set(true);
|
||||
}
|
||||
public resolvedClose(state: State<TLGState>) {
|
||||
state.resolvedIsOpen.set(false);
|
||||
}
|
||||
public getResponse(state: State<TLGState>, device: string): TQueryResponse | null {
|
||||
if (device in state.responses) {
|
||||
return state.responses[device].value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public formReady(state: State<TLGState>): boolean {
|
||||
return (
|
||||
state.isSubmitting.value &&
|
||||
all(
|
||||
...[
|
||||
state.queryVrf.value !== '',
|
||||
state.queryType.value !== '',
|
||||
state.queryTarget.value !== '',
|
||||
state.queryLocation.length !== 0,
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
public resetForm(state: State<TLGState>) {
|
||||
state.merge({
|
||||
queryVrf: '',
|
||||
families: [],
|
||||
queryType: '',
|
||||
responses: {},
|
||||
queryTarget: '',
|
||||
queryLocation: [],
|
||||
displayTarget: '',
|
||||
btnLoading: false,
|
||||
isSubmitting: false,
|
||||
resolvedIsOpen: false,
|
||||
availVrfs: [],
|
||||
selections: { queryLocation: [], queryType: null, queryVrf: null },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function Methods(): Plugin;
|
||||
function Methods(inst: State<TLGState>): MethodsExtension;
|
||||
function Methods(inst?: State<TLGState>): Plugin | MethodsExtension {
|
||||
if (inst) {
|
||||
const [instance] = inst.attach(PluginID) as [
|
||||
MethodsInstance | Error,
|
||||
PluginStateControl<TLGState>,
|
||||
];
|
||||
|
||||
if (instance instanceof Error) {
|
||||
throw instance;
|
||||
}
|
||||
|
||||
return {
|
||||
resetForm: () => instance.resetForm(inst),
|
||||
formReady: () => instance.formReady(inst),
|
||||
resolvedOpen: () => instance.resolvedOpen(inst),
|
||||
resolvedClose: () => instance.resolvedClose(inst),
|
||||
getResponse: device => instance.getResponse(inst, device),
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: PluginID,
|
||||
init: () => {
|
||||
return new MethodsInstance() as {};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface TSelections {
|
||||
queryLocation: TSelectOption[] | [];
|
||||
queryType: TSelectOption | null;
|
||||
queryVrf: TSelectOption | null;
|
||||
}
|
||||
|
||||
type TLGState = {
|
||||
queryVrf: string;
|
||||
families: Families;
|
||||
queryTarget: string;
|
||||
btnLoading: boolean;
|
||||
formData: TFormData;
|
||||
isSubmitting: boolean;
|
||||
displayTarget: string;
|
||||
queryType: TQueryTypes;
|
||||
queryLocation: string[];
|
||||
availVrfs: TDeviceVrf[];
|
||||
resolvedIsOpen: boolean;
|
||||
fqdnTarget: string | null;
|
||||
selections: TSelections;
|
||||
responses: { [d: string]: TQueryResponse };
|
||||
};
|
||||
|
||||
type TLGStateHandlers = {
|
||||
resolvedOpen(): void;
|
||||
resolvedClose(): void;
|
||||
resetForm(): void;
|
||||
exportState<S extends unknown | null>(s: S): S | null;
|
||||
getResponse(d: string): TQueryResponse | null;
|
||||
resolvedClose(): void;
|
||||
resolvedOpen(): void;
|
||||
formReady(): boolean;
|
||||
resetForm(): void;
|
||||
};
|
||||
|
||||
const LGState = createState<TLGState>({
|
||||
formData: { query_location: [], query_target: '', query_type: '', query_vrf: '' },
|
||||
selections: { queryLocation: [], queryType: null, queryVrf: null },
|
||||
resolvedIsOpen: false,
|
||||
isSubmitting: false,
|
||||
displayTarget: '',
|
||||
queryLocation: [],
|
||||
btnLoading: false,
|
||||
fqdnTarget: null,
|
||||
queryTarget: '',
|
||||
queryType: '',
|
||||
availVrfs: [],
|
||||
|
|
@ -42,36 +138,31 @@ const LGState = createState<TLGState>({
|
|||
families: [],
|
||||
});
|
||||
|
||||
export function useLGState(): State<TLGState> & TLGStateHandlers {
|
||||
const state = useState<TLGState>(LGState);
|
||||
function resolvedOpen() {
|
||||
state.resolvedIsOpen.set(true);
|
||||
}
|
||||
function resolvedClose() {
|
||||
state.resolvedIsOpen.set(false);
|
||||
}
|
||||
function resetForm() {
|
||||
state.merge({
|
||||
queryVrf: '',
|
||||
families: [],
|
||||
queryType: '',
|
||||
queryTarget: '',
|
||||
fqdnTarget: null,
|
||||
queryLocation: [],
|
||||
displayTarget: '',
|
||||
resolvedIsOpen: false,
|
||||
btnLoading: false,
|
||||
formData: { query_location: [], query_target: '', query_type: '', query_vrf: '' },
|
||||
responses: {},
|
||||
});
|
||||
}
|
||||
function getResponse(device: string): TQueryResponse | null {
|
||||
if (device in state.responses) {
|
||||
return state.responses[device].value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return { resetForm, resolvedOpen, resolvedClose, getResponse, ...state };
|
||||
export function useLGState(): State<TLGState> {
|
||||
return useState<TLGState>(LGState);
|
||||
}
|
||||
|
||||
function stateExporter<O extends unknown>(obj: O): O | null {
|
||||
let result = null;
|
||||
if (obj === null) {
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
result = JSON.parse(JSON.stringify(obj));
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function useLGMethods(): TLGStateHandlers {
|
||||
const state = useLGState();
|
||||
state.attach(Methods);
|
||||
const exporter = useCallback(stateExporter, [isEqual]);
|
||||
return {
|
||||
exportState(s) {
|
||||
return exporter(s);
|
||||
},
|
||||
...Methods(state),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import type { State, InferredStateKeysType } from '@hookstate/core';
|
||||
import type { TValidQueryTypes, TStringTableData, TQueryResponseString } from './data';
|
||||
import type { TSelectOption } from './common';
|
||||
import type { TQueryContent } from './config';
|
||||
|
||||
export function isQueryType(q: any): q is TValidQueryTypes {
|
||||
|
|
@ -27,3 +29,29 @@ export function isStringOutput(data: any): data is TQueryResponseString {
|
|||
export function isQueryContent(c: any): c is TQueryContent {
|
||||
return typeof c !== 'undefined' && c !== null && 'content' in c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an object is a Select option.
|
||||
*/
|
||||
export function isSelectOption(a: any): a is NonNullable<TSelectOption> {
|
||||
return typeof a !== 'undefined' && a !== null && 'label' in a && 'value' in a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an object is a HookState Proxy.
|
||||
*/
|
||||
export function isState<S>(a: any): a is State<NonNullable<S>> {
|
||||
let result = false;
|
||||
if (typeof a !== 'undefined' && a !== null) {
|
||||
if (
|
||||
'get' in a &&
|
||||
typeof a.get === 'function' &&
|
||||
'set' in a &&
|
||||
typeof a.set === 'function' &&
|
||||
'promised' in a
|
||||
) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue