import { useCallback, useEffect, useMemo } from 'react'; import isEqual from 'react-fast-compare'; import { chakra, Flex, ScaleFade, SlideFade } from '@chakra-ui/react'; import { FormProvider, useForm } from 'react-hook-form'; import { vestResolver } from '@hookform/resolvers/vest'; import vest, { test, enforce } from 'vest'; import { FormRow, FormField, DirectiveInfoModal, QueryType, QueryTarget, SubmitButton, QueryLocation, } from '~/components'; import { useConfig } from '~/context'; import { useStrf, useGreeting, useDevice, useFormState } from '~/hooks'; import { isString, isQueryField, Directive } from '~/types'; import type { FormData, OnChangeArgs } from '~/types'; /** * 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 LookingGlass = (): JSX.Element => { const { web, messages } = useConfig(); const greetingReady = useGreeting(s => s.greetingReady); const getDevice = useDevice(); const strF = useStrf(); const setLoading = useFormState(s => s.setLoading); const setStatus = useFormState(s => s.setStatus); const locationChange = useFormState(s => s.locationChange); const setTarget = useFormState(s => s.setTarget); const setFormValue = useFormState(s => s.setFormValue); const { form, filtered, selections } = useFormState( useCallback(({ form, filtered, selections }) => ({ form, filtered, selections }), []), isEqual, ); const getDirective = useFormState(useCallback(s => s.getDirective, [])); const resolvedOpen = useFormState(useCallback(s => s.resolvedOpen, [])); const resetForm = useFormState(useCallback(s => s.reset, [])); const noQueryType = strF(messages.noInput, { field: web.text.queryType }); const noQueryLoc = strF(messages.noInput, { field: web.text.queryLocation }); const noQueryTarget = strF(messages.noInput, { field: web.text.queryTarget }); const queryTypes = useMemo(() => filtered.types.map(t => t.id), [filtered.types]); const formSchema = vest.create((data: FormData = {} as FormData) => { test('queryLocation', noQueryLoc, () => { enforce(data.queryLocation).isArrayOf(enforce.isString()).isNotEmpty(); }); test('queryTarget', noQueryTarget, () => { enforce(data.queryTarget).longerThan(1); }); test('queryType', noQueryType, () => { enforce(data.queryType).inside(queryTypes); }); }); const formInstance = useForm({ resolver: vestResolver(formSchema), defaultValues: { queryTarget: '', queryLocation: [], queryType: '', }, }); const { handleSubmit, register, setValue, setError, clearErrors } = formInstance; // const isFqdnQuery = useIsFqdn(form.queryTarget, form.queryType); const isFqdnQuery = useCallback( (target: string, fieldType: Directive['fieldType'] | null): boolean => fieldType === 'text' && fqdnPattern.test(target), [], ); const directive = useMemo( () => getDirective(), // eslint-disable-next-line react-hooks/exhaustive-deps [form.queryType, form.queryLocation, getDirective], ); function submitHandler(): void { console.table({ 'Query Location': form.queryLocation.toString(), 'Query Type': form.queryType, 'Query Target': form.queryTarget, 'Selected Directive': directive?.name ?? null, }); // 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(); return; } // Determine if queryTarget is an FQDN. const isFqdn = isFqdnQuery(form.queryTarget, directive?.fieldType ?? null); if (greetingReady && !isFqdn) { return setStatus('results'); } if (greetingReady && isFqdn) { setLoading(true); return resolvedOpen(); } else { console.group('%cSomething went wrong', 'color:red;'); console.table({ 'Greeting Required': web.greeting.required, 'Greeting Ready': greetingReady, 'Query Target': form.queryTarget, 'Query Type': form.queryType, 'Is FQDN': isFqdn, }); console.groupEnd(); } } const handleLocChange = (locations: string[]) => locationChange(locations, { setError, clearErrors, getDevice, text: web.text }); function handleChange(e: OnChangeArgs): void { // Signal the field & value to react-hook-form. if (isQueryField(e.field)) { setValue(e.field, e.value); } else { throw new Error(`Field '${e.field}' is not a valid form field.`); } if (e.field === 'queryLocation' && Array.isArray(e.value)) { handleLocChange(e.value); } else if (e.field === 'queryType' && isString(e.value)) { setValue('queryType', e.value); setFormValue('queryType', e.value); if (form.queryTarget !== '') { // 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. setFormValue('queryTarget', ''); setTarget({ display: '' }); } } else if (e.field === 'queryTarget' && isString(e.value)) { setFormValue('queryTarget', e.value); } } useEffect(() => { register('queryLocation', { required: true }); register('queryType', { required: true }); }, [register]); return ( 0} unmountOnExit> ) } > {directive !== null && ( )} ); };