diff --git a/hyperglass/models/config/web.py b/hyperglass/models/config/web.py
index 33cad58..1a91a2d 100644
--- a/hyperglass/models/config/web.py
+++ b/hyperglass/models/config/web.py
@@ -125,6 +125,7 @@ class Text(HyperglassModel):
query_target: StrictStr = "Target"
query_vrf: StrictStr = "Routing Table"
fqdn_tooltip: StrictStr = "Use {protocol}" # Formatted by Javascript
+ fqdn_message: StrictStr = "Your browser has resolved {fqdn} to" # Formatted by Javascript
cache_prefix: StrictStr = "Results cached for "
cache_icon: StrictStr = "Cached from {time} UTC" # Formatted by Javascript
complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript
diff --git a/hyperglass/ui/components/Markdown/elements.tsx b/hyperglass/ui/components/Markdown/elements.tsx
index 32b118d..37f7e57 100644
--- a/hyperglass/ui/components/Markdown/elements.tsx
+++ b/hyperglass/ui/components/Markdown/elements.tsx
@@ -82,6 +82,6 @@ export const TableData = (props: TTableData) => {
};
export const Paragraph = (props: TextProps) => ;
-export const InlineCode = (props: CodeProps) => ;
+export const InlineCode = (props: CodeProps) => ;
export const Divider = (props: DividerProps) => ;
export const Table = (props: BoxProps) => ;
diff --git a/hyperglass/ui/components/Markdown/types.ts b/hyperglass/ui/components/Markdown/types.ts
index 08f9da9..f0b5b71 100644
--- a/hyperglass/ui/components/Markdown/types.ts
+++ b/hyperglass/ui/components/Markdown/types.ts
@@ -14,8 +14,9 @@ export interface TCheckbox extends CheckboxProps {
checked: boolean;
}
-export interface TListItem extends ListItemProps {
+export interface TListItem {
checked: boolean;
+ children?: React.ReactNode;
}
export interface TList extends ListProps {
diff --git a/hyperglass/ui/components/Table/main.tsx b/hyperglass/ui/components/Table/main.tsx
index fb48453..d94a4b8 100644
--- a/hyperglass/ui/components/Table/main.tsx
+++ b/hyperglass/ui/components/Table/main.tsx
@@ -1,4 +1,3 @@
-import * as React from 'react';
import dynamic from 'next/dynamic';
import { Flex, Icon, Text } from '@chakra-ui/react';
import { usePagination, useSortBy, useTable } from 'react-table';
@@ -12,7 +11,8 @@ import { TableBody } from './body';
import { TableIconButton } from './button';
import { TableSelectShow } from './pageSelect';
-import type { TableOptions, PluginHook, Row } from 'react-table';
+import type { TableOptions, PluginHook } from 'react-table';
+import type { TCellRender } from '~/types';
import type { TTable } from './types';
const ChevronRight = dynamic(() =>
@@ -39,7 +39,7 @@ export function Table(props: TTable) {
data,
columns,
heading,
- cellRender,
+ Cell,
rowHighlightBg,
striped = false,
rowHighlightProp,
@@ -132,13 +132,17 @@ export function Table(props: TTable) {
highlight={row.values[rowHighlightProp ?? ''] ?? false}
{...row.getRowProps()}>
{row.cells.map((cell, i) => {
+ const { column, row, value } = cell as TCellRender;
return (
- {/* {cellRender ?? cell.render('Cell')} */}
- {React.createElement(cellRender, cell)}
+ {typeof Cell !== 'undefined' ? (
+ |
+ ) : (
+ cell.render('Cell')
+ )}
);
})}
diff --git a/hyperglass/ui/components/Table/types.ts b/hyperglass/ui/components/Table/types.ts
index e98d60d..e0ced51 100644
--- a/hyperglass/ui/components/Table/types.ts
+++ b/hyperglass/ui/components/Table/types.ts
@@ -1,6 +1,6 @@
import type { BoxProps, IconButtonProps } from '@chakra-ui/react';
-import type { Colors, TColumn } from '~/types';
+import type { Colors, TColumn, TCellRender } from '~/types';
export interface TTable {
columns: TColumn[];
@@ -9,7 +9,7 @@ export interface TTable {
striped?: boolean;
bordersVertical?: boolean;
bordersHorizontal?: boolean;
- cellRender?: React.ReactNode;
+ Cell?: React.FC;
rowHighlightProp?: keyof IRoute;
rowHighlightBg?: keyof Colors;
}
diff --git a/hyperglass/ui/components/buttons/copy.tsx b/hyperglass/ui/components/buttons/copy.tsx
index aab49ff..6afb4e2 100644
--- a/hyperglass/ui/components/buttons/copy.tsx
+++ b/hyperglass/ui/components/buttons/copy.tsx
@@ -1,6 +1,5 @@
import dynamic from 'next/dynamic';
import { Button, Icon, Tooltip, useClipboard } from '@chakra-ui/react';
-import { If } from '~/components';
const Copy = dynamic(() => import('@meronex/icons/fi').then(i => i.FiCopy));
const Check = dynamic(() => import('@meronex/icons/fi').then(i => i.FiCheck));
@@ -18,15 +17,9 @@ export const CopyButton = (props: TCopyButton) => {
size="sm"
variant="ghost"
onClick={onCopy}
- zIndex="dropdown"
- colorScheme="secondary"
+ colorScheme="primary"
{...rest}>
-
-
-
-
-
-
+
);
diff --git a/hyperglass/ui/components/buttons/requery.tsx b/hyperglass/ui/components/buttons/requery.tsx
index ba373f5..88bed26 100644
--- a/hyperglass/ui/components/buttons/requery.tsx
+++ b/hyperglass/ui/components/buttons/requery.tsx
@@ -1,3 +1,4 @@
+import { forwardRef } from 'react';
import dynamic from 'next/dynamic';
import { Button, Icon, Tooltip } from '@chakra-ui/react';
@@ -5,13 +6,23 @@ import type { TRequeryButton } from './types';
const Repeat = dynamic(() => import('@meronex/icons/fi').then(i => i.FiRepeat));
-export const RequeryButton = (props: TRequeryButton) => {
- const { requery, bg = 'secondary', ...rest } = props;
+export const RequeryButton = forwardRef((props, ref) => {
+ const { requery, ...rest } = props;
+
return (
-
);
-};
+});
diff --git a/hyperglass/ui/components/buttons/submit.tsx b/hyperglass/ui/components/buttons/submit.tsx
index 56ee636..c34b180 100644
--- a/hyperglass/ui/components/buttons/submit.tsx
+++ b/hyperglass/ui/components/buttons/submit.tsx
@@ -1,107 +1,51 @@
-import { forwardRef } from 'react';
-import { Box, Spinner } from '@chakra-ui/react';
+import {
+ IconButton,
+ Popover,
+ PopoverTrigger,
+ PopoverArrow,
+ PopoverCloseButton,
+ PopoverBody,
+ PopoverContent,
+} from '@chakra-ui/react';
import { FiSearch } from '@meronex/icons/fi';
-import { useColorValue } from '~/context';
-import { useOpposingColor } from '~/hooks';
+import { ResolvedTarget } from '~/components';
+import { useLGState } from '~/hooks';
-import type { TSubmitButton, TButtonSizeMap } from './types';
+import type { TSubmitButton } from './types';
-const btnSizeMap = {
- lg: {
- height: 12,
- minWidth: 12,
- fontSize: 'lg',
- px: 6,
- },
- md: {
- height: 10,
- minWidth: 10,
- fontSize: 'md',
- px: 4,
- },
- sm: {
- height: 8,
- minWidth: 8,
- fontSize: 'sm',
- px: 3,
- },
- xs: {
- height: 6,
- minWidth: 6,
- fontSize: 'xs',
- px: 2,
- },
-} as TButtonSizeMap;
+export const SubmitButton = (props: TSubmitButton) => {
+ const { children, handleChange, ...rest } = props;
+ const { btnLoading, resolvedIsOpen, resolvedClose } = useLGState();
-export const SubmitButton = forwardRef((props, ref) => {
- const {
- isLoading = false,
- isDisabled = false,
- isActive = false,
- isFullWidth = false,
- size = 'lg',
- loadingText,
- children,
- ...rest
- } = props;
- const _isDisabled = isDisabled || isLoading;
-
- const bg = useColorValue('primary.400', 'primary.500');
- const bgActive = useColorValue('primary.500', 'primary.600');
- const bgHover = useColorValue('primary.300', 'primary.400');
- const color = useOpposingColor(bg);
- const colorActive = useOpposingColor(bgActive);
- const colorHover = useOpposingColor(bgHover);
-
- const btnSize = btnSizeMap[size];
+ function handleClose(): void {
+ btnLoading.set(false);
+ resolvedClose();
+ }
return (
-
- {isLoading ? (
-
- ) : (
-
- )}
- {isLoading
- ? loadingText || (
-
- {children}
-
- )
- : children}
-
+ <>
+
+
+ }
+ title="Submit Query"
+ aria-label="Submit Query"
+ colorScheme="primary"
+ isLoading={btnLoading.value}
+ {...rest}
+ />
+
+
+
+
+
+ {resolvedIsOpen.value && }
+
+
+
+ >
);
-});
+};
diff --git a/hyperglass/ui/components/buttons/types.ts b/hyperglass/ui/components/buttons/types.ts
index 7660523..fe80fd3 100644
--- a/hyperglass/ui/components/buttons/types.ts
+++ b/hyperglass/ui/components/buttons/types.ts
@@ -1,4 +1,5 @@
-import type { BoxProps, ButtonProps } from '@chakra-ui/react';
+import type { IconButtonProps, ButtonProps } from '@chakra-ui/react';
+import type { OnChangeArgs } from '~/types';
export interface TCopyButton extends ButtonProps {
copyValue: string;
@@ -7,20 +8,9 @@ export interface TCopyButton extends ButtonProps {
export interface TColorModeToggle extends ButtonProps {
size?: string;
}
-export type TButtonSizeMap = {
- xs: BoxProps;
- sm: BoxProps;
- md: BoxProps;
- lg: BoxProps;
-};
-export interface TSubmitButton extends BoxProps {
- isLoading?: boolean;
- isDisabled?: boolean;
- isActive?: boolean;
- isFullWidth?: boolean;
- size?: keyof TButtonSizeMap;
- loadingText?: string;
+export interface TSubmitButton extends Omit {
+ handleChange(e: OnChangeArgs): void;
}
export interface TRequeryButton extends ButtonProps {
diff --git a/hyperglass/ui/components/form/communitySelect.tsx b/hyperglass/ui/components/form/communitySelect.tsx
index c1cfe37..714f219 100644
--- a/hyperglass/ui/components/form/communitySelect.tsx
+++ b/hyperglass/ui/components/form/communitySelect.tsx
@@ -15,7 +15,7 @@ function buildOptions(communities: TBGPCommunity[]): TSelectOption[] {
}));
}
-const Option = (props: OptionProps) => {
+const Option = (props: OptionProps) => {
const { label, data } = props;
return (
diff --git a/hyperglass/ui/components/form/queryLocation.tsx b/hyperglass/ui/components/form/queryLocation.tsx
index cfd60df..98b9cc3 100644
--- a/hyperglass/ui/components/form/queryLocation.tsx
+++ b/hyperglass/ui/components/form/queryLocation.tsx
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { Select } from '~/components';
import { useConfig } from '~/context';
-import type { TNetwork, TSelectOptionMulti } from '~/types';
+import type { TNetwork, TSelectOption } from '~/types';
import type { TQuerySelectField } from './types';
function buildOptions(networks: TNetwork[]) {
@@ -23,12 +23,16 @@ export const QueryLocation = (props: TQuerySelectField) => {
const { networks } = useConfig();
const options = useMemo(() => buildOptions(networks), [networks.length]);
- function handleChange(e: TSelectOptionMulti): void {
+ function handleChange(e: TSelectOption | TSelectOption[]): void {
if (e === null) {
e = [];
+ } else if (typeof e === 'string') {
+ e = [e];
+ }
+ if (Array.isArray(e)) {
+ const value = e.map(sel => sel!.value);
+ onChange({ field: 'query_location', value });
}
- const value = e.map(sel => sel.value);
- onChange({ field: 'query_location', value });
}
return (
diff --git a/hyperglass/ui/components/form/queryType.tsx b/hyperglass/ui/components/form/queryType.tsx
index 42418f3..94c0ee9 100644
--- a/hyperglass/ui/components/form/queryType.tsx
+++ b/hyperglass/ui/components/form/queryType.tsx
@@ -17,8 +17,8 @@ export const QueryType = (props: TQuerySelectField) => {
const options = useMemo(() => buildOptions(queries.list), [queries.list.length]);
- function handleChange(e: TSelectOption): void {
- if (e !== null) {
+ function handleChange(e: TSelectOption | TSelectOption[]): void {
+ if (!Array.isArray(e) && e !== null) {
onChange({ field: 'query_type', value: e.value });
}
}
diff --git a/hyperglass/ui/components/form/queryVrf.tsx b/hyperglass/ui/components/form/queryVrf.tsx
index c82ddab..4c6354e 100644
--- a/hyperglass/ui/components/form/queryVrf.tsx
+++ b/hyperglass/ui/components/form/queryVrf.tsx
@@ -13,8 +13,8 @@ export const QueryVrf = (props: TQueryVrf) => {
const options = useMemo(() => buildOptions(vrfs), [vrfs.length]);
- function handleChange(e: TSelectOption): void {
- if (e !== null) {
+ function handleChange(e: TSelectOption | TSelectOption[]): void {
+ if (!Array.isArray(e) && e !== null) {
onChange({ field: 'query_vrf', value: e.value });
}
}
diff --git a/hyperglass/ui/components/form/resolvedTarget.tsx b/hyperglass/ui/components/form/resolvedTarget.tsx
index 7880c94..f9d0f51 100644
--- a/hyperglass/ui/components/form/resolvedTarget.tsx
+++ b/hyperglass/ui/components/form/resolvedTarget.tsx
@@ -1,10 +1,11 @@
-import { useEffect } from 'react';
-import { Button, Icon, Spinner, Stack, Tag, Text, Tooltip } from '@chakra-ui/react';
+import { useEffect, useMemo } from 'react';
+import { Button, Stack, Text, VStack } from '@chakra-ui/react';
import { useQuery } from 'react-query';
-import { useConfig } from '~/context';
-import { useStrf } from '~/hooks';
+import { FiArrowRightCircle as RightArrow } from '@meronex/icons/fi';
+import { useConfig, useColorValue, useGlobalState } from '~/context';
+import { useStrf, useLGState } from '~/hooks';
-import type { DnsOverHttps, ColorNames } from '~/types';
+import type { DnsOverHttps } from '~/types';
import type { TResolvedTarget } from './types';
function findAnswer(data: DnsOverHttps.Response | undefined): string {
@@ -17,26 +18,32 @@ function findAnswer(data: DnsOverHttps.Response | undefined): string {
}
export const ResolvedTarget = (props: TResolvedTarget) => {
- const { fqdnTarget, setTarget, queryTarget, families, availVrfs } = props;
+ const { setTarget } = props;
const { web } = useConfig();
+ const { isSubmitting } = useGlobalState();
+ const { fqdnTarget, queryTarget, families, formData } = useLGState();
+
+ const color = useColorValue('secondary.500', 'secondary.300');
const dnsUrl = web.dns_provider.url;
- const query4 = Array.from(families).includes(4);
- const query6 = Array.from(families).includes(6);
+ const query4 = Array.from(families.value).includes(4);
+ const query6 = Array.from(families.value).includes(6);
const tooltip4 = useStrf(web.text.fqdn_tooltip, { protocol: 'IPv4' });
const tooltip6 = useStrf(web.text.fqdn_tooltip, { protocol: 'IPv6' });
+ const [messageStart, messageEnd] = useMemo(() => web.text.fqdn_message.split('{fqdn}'), [
+ web.text.fqdn_message,
+ ]);
const { data: data4, isLoading: isLoading4, isError: isError4 } = useQuery(
- [fqdnTarget, 4],
+ [fqdnTarget.value, 4],
dnsQuery,
);
const { data: data6, isLoading: isLoading6, isError: isError6 } = useQuery(
- [fqdnTarget, 6],
+ [fqdnTarget.value, 6],
dnsQuery,
);
-
async function dnsQuery(
target: string,
family: 4 | 6,
@@ -58,13 +65,9 @@ export const ResolvedTarget = (props: TResolvedTarget) => {
function handleOverride(value: string): void {
setTarget({ field: 'query_target', value });
}
-
- function isSelected(value: string): ColorNames {
- if (value === queryTarget) {
- return 'success';
- } else {
- return 'secondary';
- }
+ function selectTarget(value: string): void {
+ formData.set(p => ({ ...p, query_target: value }));
+ isSubmitting.set(true);
}
useEffect(() => {
@@ -78,69 +81,42 @@ export const ResolvedTarget = (props: TResolvedTarget) => {
}, [data4, data6]);
return (
- 1
- ? 'space-between'
- : 'flex-end'
- }
- flexWrap="wrap">
- {isLoading4 ||
- isError4 ||
- (query4 && findAnswer(data4) && (
-
-
- handleOverride(findAnswer(data4))}>
- IPv4
-
-
- {isLoading4 && }
- {isError4 && }
- {findAnswer(data4) && (
-
- {findAnswer(data4)}
-
- )}
-
- ))}
- {isLoading6 ||
- isError6 ||
- (query6 && findAnswer(data6) && (
-
-
- handleOverride(findAnswer(data6))}>
- IPv6
-
-
- {isLoading6 && }
- {isError6 && }
- {findAnswer(data6) && (
-
- {findAnswer(data6)}
-
- )}
-
- ))}
-
+
+
+ {messageStart}
+
+ {fqdnTarget.value}
+
+ {messageEnd}
+
+
+ {!isLoading4 && !isError4 && query4 && findAnswer(data4) && (
+ }
+ title={tooltip4}
+ fontFamily="mono"
+ onClick={() => selectTarget(findAnswer(data4))}>
+ {findAnswer(data4)}
+
+ )}
+ {!isLoading6 && !isError6 && query6 && findAnswer(data6) && (
+ }
+ title={tooltip6}
+ fontFamily="mono"
+ onClick={() => selectTarget(findAnswer(data6))}>
+ {findAnswer(data6)}
+
+ )}
+
+
);
};
diff --git a/hyperglass/ui/components/form/types.ts b/hyperglass/ui/components/form/types.ts
index 740c546..63acab3 100644
--- a/hyperglass/ui/components/form/types.ts
+++ b/hyperglass/ui/components/form/types.ts
@@ -1,6 +1,6 @@
import type { FormControlProps } from '@chakra-ui/react';
import type { FieldError, Control } from 'react-hook-form';
-import type { TDeviceVrf, TBGPCommunity, OnChangeArgs, Families, TFormData } from '~/types';
+import type { TDeviceVrf, TBGPCommunity, OnChangeArgs, TFormData } from '~/types';
export interface TField extends FormControlProps {
name: string;
@@ -59,9 +59,5 @@ export interface TQueryTarget {
}
export interface TResolvedTarget {
- families: Families;
- queryTarget: string;
- availVrfs: TDeviceVrf[];
- fqdnTarget: string | null;
setTarget(e: OnChangeArgs): void;
}
diff --git a/hyperglass/ui/components/header/header.tsx b/hyperglass/ui/components/header/header.tsx
index 0488d72..59ce102 100644
--- a/hyperglass/ui/components/header/header.tsx
+++ b/hyperglass/ui/components/header/header.tsx
@@ -75,7 +75,7 @@ export const Header = (props: THeader) => {
const resetButton = (
{
key="title"
px={1}
alignItems={isSubmitting ? 'center' : ['center', 'center', 'flex-end', 'flex-end']}
- positionTransition={headerTransition}
+ transition={headerTransition}
initial={{ scale: 0.5 }}
animate={
isSubmitting && web.text.title_mode === 'text_only'
@@ -114,7 +114,7 @@ export const Header = (props: THeader) => {
);
const colorModeToggle = (
{
- const { isSubmitting, formData } = useGlobalState();
-
+ const { isSubmitting } = useGlobalState();
+ const { formData } = useLGState();
return (
{
formData.query_vrf.value,
)
}>
-
+
diff --git a/hyperglass/ui/components/lookingGlass.tsx b/hyperglass/ui/components/lookingGlass.tsx
index 9275b27..490762a 100644
--- a/hyperglass/ui/components/lookingGlass.tsx
+++ b/hyperglass/ui/components/lookingGlass.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useMemo } from 'react';
import { Flex } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { intersectionWith } from 'lodash';
@@ -14,19 +14,20 @@ import {
QueryTarget,
SubmitButton,
QueryLocation,
- ResolvedTarget,
CommunitySelect,
} from '~/components';
import { useConfig, useGlobalState } from '~/context';
-import { useStrf, useGreeting, useDevice } from '~/hooks';
+import { useStrf, useGreeting, useDevice, useLGState } from '~/hooks';
import { isQueryType, isString } from '~/types';
-import type { Families, TFormData, TDeviceVrf, TQueryTypes, OnChangeArgs } 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;
export const HyperglassForm = () => {
const { web, content, messages, queries } = useConfig();
- const { formData, isSubmitting } = useGlobalState();
+ const { isSubmitting } = useGlobalState();
const [greetingAck, setGreetingAck] = useGreeting();
const getDevice = useDevice();
@@ -41,24 +42,34 @@ export const HyperglassForm = () => {
query_target: yup.string().required(noQueryTarget),
});
- const { handleSubmit, register, unregister, setValue, errors } = useForm({
+ const { handleSubmit, register, unregister, setValue, errors, reset } = useForm({
validationSchema: formSchema,
- defaultValues: { query_vrf: 'default', query_target: '' },
+ defaultValues: { query_vrf: 'default', query_target: '', query_location: [], query_type: '' },
});
- const [queryLocation, setQueryLocation] = useState([]);
- const [queryType, setQueryType] = useState('');
- const [queryVrf, setQueryVrf] = useState('');
- const [queryTarget, setQueryTarget] = useState('');
- const [availVrfs, setAvailVrfs] = useState([]);
- const [fqdnTarget, setFqdnTarget] = useState('');
- const [displayTarget, setDisplayTarget] = useState('');
- const [families, setFamilies] = useState([]);
+ const {
+ queryVrf,
+ families,
+ queryType,
+ availVrfs,
+ fqdnTarget,
+ btnLoading,
+ queryTarget,
+ resolvedOpen,
+ queryLocation,
+ displayTarget,
+ formData,
+ } = useLGState();
- function onSubmit(values: TFormData): void {
+ function submitHandler(values: TFormData) {
if (!greetingAck && web.greeting.required) {
window.location.reload(false);
setGreetingAck(false);
+ } else if (fqdnPattern.test(values.query_target)) {
+ btnLoading.set(true);
+ fqdnTarget.set(values.query_target);
+ formData.set(values);
+ resolvedOpen();
} else {
formData.set(values);
isSubmitting.set(true);
@@ -68,7 +79,7 @@ export const HyperglassForm = () => {
function handleLocChange(locations: string[]): void {
const allVrfs = [] as TDeviceVrf[][];
- setQueryLocation(locations);
+ queryLocation.set(locations);
// Create an array of each device's VRFs.
for (const loc of locations) {
@@ -82,11 +93,14 @@ export const HyperglassForm = () => {
(a: TDeviceVrf, b: TDeviceVrf) => a.id === b.id,
);
- setAvailVrfs(intersecting);
+ availVrfs.set(intersecting);
// If there are no intersecting VRFs, use the default VRF.
- if (intersecting.filter(i => i.id === queryVrf).length === 0 && queryVrf !== 'default') {
- setQueryVrf('default');
+ if (
+ intersecting.filter(i => i.id === queryVrf.value).length === 0 &&
+ queryVrf.value !== 'default'
+ ) {
+ queryVrf.set('default');
}
let ipv4 = 0;
@@ -104,13 +118,13 @@ export const HyperglassForm = () => {
}
if (ipv4 !== 0 && ipv4 === ipv6) {
- setFamilies([4, 6]);
+ families.set([4, 6]);
} else if (ipv4 > ipv6) {
- setFamilies([4]);
+ families.set([4]);
} else if (ipv4 < ipv6) {
- setFamilies([6]);
+ families.set([6]);
} else {
- setFamilies([]);
+ families.set([]);
}
}
@@ -120,35 +134,35 @@ export const HyperglassForm = () => {
if (e.field === 'query_location' && Array.isArray(e.value)) {
handleLocChange(e.value);
} else if (e.field === 'query_type' && isQueryType(e.value)) {
- setQueryType(e.value);
+ queryType.set(e.value);
} else if (e.field === 'query_vrf' && isString(e.value)) {
- setQueryVrf(e.value);
+ queryVrf.set(e.value);
} else if (e.field === 'query_target' && isString(e.value)) {
- setQueryTarget(e.value);
+ queryTarget.set(e.value);
}
}
const vrfContent = useMemo(() => {
- if (Object.keys(content.vrf).includes(queryVrf) && queryType !== '') {
- return content.vrf[queryVrf][queryType];
+ if (Object.keys(content.vrf).includes(queryVrf.value) && queryType.value !== '') {
+ return content.vrf[queryVrf.value][queryType.value];
}
}, [queryVrf]);
const isFqdnQuery = useMemo(() => {
- return ['bgp_route', 'ping', 'traceroute'].includes(queryType);
+ return ['bgp_route', 'ping', 'traceroute'].includes(queryType.value);
}, [queryType]);
const fqdnQuery = useMemo(() => {
let result = null;
- if (fqdnTarget && queryVrf === 'default' && fqdnTarget) {
+ if (fqdnTarget && queryVrf.value === 'default' && fqdnTarget) {
result = fqdnTarget;
}
return result;
}, [queryVrf, queryType]);
useEffect(() => {
- register({ name: 'query_location' });
- register({ name: 'query_type' });
+ register({ name: 'query_location', required: true });
+ register({ name: 'query_type', required: true });
register({ name: 'query_vrf' });
}, [register]);
@@ -165,7 +179,7 @@ export const HyperglassForm = () => {
transition={{ duration: 0.3 }}
exit={{ opacity: 0, x: -300 }}
initial={{ opacity: 0, y: 300 }}
- onSubmit={handleSubmit(onSubmit)}
+ onSubmit={handleSubmit(submitHandler)}
maxW={{ base: '100%', lg: '75%' }}>
{
1}>
-
+
-
- )
- }>
-
+
+
{
communities={queries.bgp_community.communities}
/>
-
+
@@ -238,7 +237,7 @@ export const HyperglassForm = () => {
flex="0 0 0"
flexDir="column"
mr={{ base: 0, lg: 2 }}>
-
+
diff --git a/hyperglass/ui/components/output/fields.tsx b/hyperglass/ui/components/output/fields.tsx
index b93498d..d156a99 100644
--- a/hyperglass/ui/components/output/fields.tsx
+++ b/hyperglass/ui/components/output/fields.tsx
@@ -2,14 +2,15 @@ import { forwardRef } from 'react';
import { Icon, Text, Box, Tooltip, Menu, MenuButton, MenuList } from '@chakra-ui/react';
import { CgMoreO as More } from '@meronex/icons/cg';
import { BisError as Warning } from '@meronex/icons/bi';
-import { MdNotInterested as NotAllowed, MdLastPage } from '@meronex/icons/md';
+import { MdNotInterested as NotAllowed } from '@meronex/icons/md';
+import { RiHome2Fill as End } from '@meronex/icons/ri';
import { BsQuestionCircleFill as Question } from '@meronex/icons/bs';
import { FaCheckCircle as Check, FaChevronRight as ChevronRight } from '@meronex/icons/fa';
import dayjs from 'dayjs';
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
import utcPlugin from 'dayjs/plugin/utc';
-import { useConfig, useColorValue } from '~/context';
import { If } from '~/components';
+import { useConfig, useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import type {
@@ -85,7 +86,7 @@ export const ASPath = (props: TASPath) => {
);
if (path.length === 0) {
- return ;
+ return ;
}
let paths = [] as JSX.Element[];
@@ -108,7 +109,8 @@ export const ASPath = (props: TASPath) => {
export const Communities = (props: TCommunities) => {
const { communities } = props;
- const color = useColorValue('black', 'white');
+ const bg = useColorValue('white', 'gray.900');
+ const color = useOpposingColor(bg);
return (
<>
@@ -123,6 +125,7 @@ export const Communities = (props: TCommunities) => {
{
columns={columns}
data={data.routes}
rowHighlightProp="active"
- cellRender={(d: CellProps) => | }
+ Cell={(d: TCellRender) => | }
bordersHorizontal
rowHighlightBg="green"
/>
diff --git a/hyperglass/ui/components/output/types.ts b/hyperglass/ui/components/output/types.ts
index 116941a..a492625 100644
--- a/hyperglass/ui/components/output/types.ts
+++ b/hyperglass/ui/components/output/types.ts
@@ -1,5 +1,5 @@
import type { BoxProps, FlexProps, TextProps } from '@chakra-ui/react';
-import type { CellProps } from 'react-table';
+import type { TCellRender } from '~/types';
export interface TTextOutput extends Omit {
children: string;
@@ -41,7 +41,7 @@ export interface TRPKIState {
}
export interface TCell {
- data: CellProps;
+ data: TCellRender;
rawData: TStructuredResponse;
}
diff --git a/hyperglass/ui/components/results/group.tsx b/hyperglass/ui/components/results/group.tsx
index 399b9c7..15b5a4e 100644
--- a/hyperglass/ui/components/results/group.tsx
+++ b/hyperglass/ui/components/results/group.tsx
@@ -3,15 +3,19 @@ import { Accordion, Box, Stack, useToken } from '@chakra-ui/react';
import { motion, AnimatePresence } from 'framer-motion';
import { AnimatedDiv, Label } from '~/components';
import { useConfig, useBreakpointValue } from '~/context';
-import { useDevice } from '~/hooks';
+import { useDevice, useLGState } from '~/hooks';
import { isQueryType } from '~/types';
import { Result } from './individual';
-import type { TResults } from './types';
-
-export const Results = (props: TResults) => {
- const { queryLocation, queryType, queryVrf, queryTarget, ...rest } = props;
- const { request_timeout, queries, vrfs, web } = useConfig();
+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 getDevice = useDevice();
const targetBg = useToken('colors', 'teal.600');
const queryBg = useToken('colors', 'cyan.500');
@@ -62,11 +66,11 @@ export const Results = (props: TResults) => {
const [resultsComplete, setComplete] = useState(null);
const matchedVrf =
- vrfs.filter(v => v.id === queryVrf)[0] ?? vrfs.filter(v => v.id === 'default')[0];
+ vrfs.filter(v => v.id === queryVrf.value)[0] ?? vrfs.filter(v => v.id === 'default')[0];
let queryTypeLabel = '';
- if (isQueryType(queryType)) {
- queryTypeLabel = queries[queryType].display_name;
+ if (isQueryType(queryType.value)) {
+ queryTypeLabel = queries[queryType.value].display_name;
}
return (
@@ -77,8 +81,7 @@ export const Results = (props: TResults) => {
w="100%"
mx="auto"
textAlign="left"
- maxW={{ base: '100%', lg: '75%', xl: '50%' }}
- {...rest}>
+ maxW={{ base: '100%', lg: '75%', xl: '50%' }}>
{queryLocation && (
@@ -102,7 +105,7 @@ export const Results = (props: TResults) => {
transition={{ duration: 0.3, delay: 0.3 }}>
@@ -138,27 +141,26 @@ export const Results = (props: TResults) => {
transition={{ duration: 0.3 }}
animate={{ opacity: 1, y: 0 }}
maxW={{ base: '100%', md: '75%' }}>
-
+
{queryLocation &&
queryLocation.map((loc, i) => {
- const device = getDevice(loc);
+ const device = getDevice(loc.value);
return (
diff --git a/hyperglass/ui/components/results/individual.tsx b/hyperglass/ui/components/results/individual.tsx
index e8920bd..b5c2ec0 100644
--- a/hyperglass/ui/components/results/individual.tsx
+++ b/hyperglass/ui/components/results/individual.tsx
@@ -145,7 +145,6 @@ export const Result = forwardRef((props, ref) => {
return (
((props, ref) => {
-
+
((props, ref) => {
) : isStringOutput(data) && data.level === 'success' && !tableComponent ? (
{data.output}
) : isStringOutput(data) && data.level !== 'success' ? (
-
+
+
+
) : null}
>
)}
- {isError && }
@@ -214,7 +214,7 @@ export const Result = forwardRef((props, ref) => {
mt={2}
justifyContent={['flex-start', 'flex-start', 'flex-end', 'flex-end']}
flex="1 0 auto">
-
+
diff --git a/hyperglass/ui/components/results/types.ts b/hyperglass/ui/components/results/types.ts
index f8819ae..9c5da5c 100644
--- a/hyperglass/ui/components/results/types.ts
+++ b/hyperglass/ui/components/results/types.ts
@@ -1,5 +1,5 @@
-import type { BoxProps, FlexProps } from '@chakra-ui/react';
-import type { TDevice, TQueryTypes, TFormState } from '~/types';
+import type { FlexProps } from '@chakra-ui/react';
+import type { TDevice, TQueryTypes } from '~/types';
export interface TResultHeader {
title: string;
@@ -26,10 +26,8 @@ export interface TResult {
queryType: TQueryTypes;
queryTarget: string;
setComplete(v: number | null): void;
- queryLocation: string[];
+ queryLocation: string;
resultsComplete: number | null;
}
-export type TResults = TFormState & BoxProps;
-
export type TErrorLevels = 'success' | 'warning' | 'error';
diff --git a/hyperglass/ui/components/select/types.ts b/hyperglass/ui/components/select/types.ts
index 48d561b..237d8d7 100644
--- a/hyperglass/ui/components/select/types.ts
+++ b/hyperglass/ui/components/select/types.ts
@@ -10,7 +10,7 @@ import type {
PlaceholderProps,
} from 'react-select';
import type { BoxProps } from '@chakra-ui/react';
-import type { ColorNames, TSelectOption, TSelectOptionGroup } from '~/types';
+import type { ColorNames, TSelectOption, TSelectOptionMulti, TSelectOptionGroup } from '~/types';
export interface TSelectState {
[k: string]: string[];
@@ -27,7 +27,7 @@ export interface TSelectBase extends TBoxAsReactSelect {
options: TOptions;
required?: boolean;
onSelect?: (s: TSelectOption[]) => void;
- onChange?: (c: TSelectOption) => void;
+ onChange?: (c: TSelectOption | TSelectOptionMulti) => void;
colorScheme?: ColorNames;
}
@@ -52,19 +52,19 @@ export interface TRSTheme extends Omit {
borderRadius: string | number;
}
-export type TControl = ControlProps;
+export type TControl = ControlProps;
-export type TMenu = MenuProps;
+export type TMenu = MenuProps;
-export type TMenuList = MenuListComponentProps;
+export type TMenuList = MenuListComponentProps;
-export type TOption = OptionProps;
+export type TOption = OptionProps;
export type TMultiValueState = MultiValueProps;
-export type TIndicator = IndicatorProps;
+export type TIndicator = IndicatorProps;
-export type TPlaceholder = PlaceholderProps;
+export type TPlaceholder = PlaceholderProps;
export type TMultiValue = Pick;
diff --git a/hyperglass/ui/components/title/title.tsx b/hyperglass/ui/components/title/title.tsx
index 75e8fe9..b7684d1 100644
--- a/hyperglass/ui/components/title/title.tsx
+++ b/hyperglass/ui/components/title/title.tsx
@@ -11,13 +11,13 @@ import type { TTitle, TTextOnly } from './types';
const TextOnly = (props: TTextOnly) => {
const { showSubtitle, ...rest } = props;
- const textAlign = useBooleanValue(
- showSubtitle,
- { base: 'right', md: 'center' },
- { base: 'left', md: 'center' },
- );
+
return (
-
+
diff --git a/hyperglass/ui/context/GlobalState.ts b/hyperglass/ui/context/GlobalState.ts
index e92b247..200be92 100644
--- a/hyperglass/ui/context/GlobalState.ts
+++ b/hyperglass/ui/context/GlobalState.ts
@@ -1,5 +1,5 @@
import { createState, useState } from '@hookstate/core';
-import type { TGlobalState } from './types';
+import type { TGlobalState, TUseGlobalState } from './types';
// const StateContext = createContext(null);
@@ -26,8 +26,23 @@ import type { TGlobalState } from './types';
// export const useHyperglassState = () => useContext(StateContext);
-export const globalState = createState({
+const defaultFormData = {
+ query_location: [],
+ query_target: '',
+ query_type: '',
+ query_vrf: '',
+} as TGlobalState['formData'];
+
+const globalState = createState({
isSubmitting: false,
- formData: { query_location: [], query_target: '', query_type: '', query_vrf: '' },
+ formData: defaultFormData,
});
-export const useGlobalState = () => useState(globalState);
+
+export function useGlobalState(): TUseGlobalState {
+ const state = useState(globalState);
+ function resetForm(): void {
+ state.formData.set(defaultFormData);
+ state.isSubmitting.set(false);
+ }
+ return { resetForm, ...state };
+}
diff --git a/hyperglass/ui/context/types.ts b/hyperglass/ui/context/types.ts
index 507f714..70f61af 100644
--- a/hyperglass/ui/context/types.ts
+++ b/hyperglass/ui/context/types.ts
@@ -1,3 +1,4 @@
+import type { State } from '@hookstate/core';
import type { IConfig, TFormData } from '~/types';
export interface THyperglassProvider {
@@ -9,3 +10,9 @@ export interface TGlobalState {
isSubmitting: boolean;
formData: TFormData;
}
+
+interface TGlobalStateFunctions {
+ resetForm(): void;
+}
+
+export type TUseGlobalState = State & TGlobalStateFunctions;
diff --git a/hyperglass/ui/hooks/index.ts b/hyperglass/ui/hooks/index.ts
index d3c8778..40439e2 100644
--- a/hyperglass/ui/hooks/index.ts
+++ b/hyperglass/ui/hooks/index.ts
@@ -6,3 +6,4 @@ export * from './useOpposingColor';
export * from './useSessionStorage';
export * from './useStrf';
export * from './useTableToString';
+export * from './useLGState';
diff --git a/hyperglass/ui/hooks/useLGQuery.ts b/hyperglass/ui/hooks/useLGQuery.ts
index 7c51430..a6d86e7 100644
--- a/hyperglass/ui/hooks/useLGQuery.ts
+++ b/hyperglass/ui/hooks/useLGQuery.ts
@@ -1,7 +1,7 @@
import { useQuery } from 'react-query';
import { useConfig } from '~/context';
-import type { TFormState } from '~/types';
+import type { TFormQuery } from '~/types';
/**
* Fetch Wrapper that incorporates a timeout via a passed AbortController instance.
@@ -30,11 +30,11 @@ export async function fetchWithTimeout(
return await fetch(uri, config);
}
-export function useLGQuery(query: TFormState) {
+export function useLGQuery(query: TFormQuery) {
const { request_timeout } = useConfig();
const controller = new AbortController();
- async function runQuery(url: string, requestData: TFormState): Promise {
+ async function runQuery(url: string, requestData: TFormQuery): Promise {
const { queryLocation, queryTarget, queryType, queryVrf } = requestData;
const res = await fetchWithTimeout(
url,
diff --git a/hyperglass/ui/hooks/useLGState.ts b/hyperglass/ui/hooks/useLGState.ts
new file mode 100644
index 0000000..2f39f81
--- /dev/null
+++ b/hyperglass/ui/hooks/useLGState.ts
@@ -0,0 +1,49 @@
+import { useState, createState } from '@hookstate/core';
+
+import type { State } from '@hookstate/core';
+import type { Families, TDeviceVrf, TQueryTypes, TFormData } from '~/types';
+
+type TLGState = {
+ queryVrf: string;
+ families: Families;
+ queryTarget: string;
+ btnLoading: boolean;
+ displayTarget: string;
+ queryType: TQueryTypes;
+ queryLocation: string[];
+ availVrfs: TDeviceVrf[];
+ resolvedIsOpen: boolean;
+ fqdnTarget: string | null;
+ formData: TFormData;
+};
+
+type TLGStateHandlers = {
+ resolvedOpen(): void;
+ resolvedClose(): void;
+};
+
+const LGState = createState({
+ resolvedIsOpen: false,
+ displayTarget: '',
+ queryLocation: [],
+ btnLoading: false,
+ fqdnTarget: null,
+ queryTarget: '',
+ queryType: '',
+ availVrfs: [],
+ queryVrf: '',
+ families: [],
+ formData: { query_location: [], query_target: '', query_type: '', query_vrf: '' },
+});
+
+export function useLGState(): State & TLGStateHandlers {
+ const state = useState(LGState);
+ function resolvedOpen() {
+ state.resolvedIsOpen.set(true);
+ }
+ function resolvedClose() {
+ state.resolvedIsOpen.set(false);
+ }
+
+ return { resolvedOpen, resolvedClose, ...state };
+}
diff --git a/hyperglass/ui/types/config.ts b/hyperglass/ui/types/config.ts
index f31a9e3..4bd15fb 100644
--- a/hyperglass/ui/types/config.ts
+++ b/hyperglass/ui/types/config.ts
@@ -35,6 +35,7 @@ export interface IConfigWebText {
query_target: string;
query_vrf: string;
fqdn_tooltip: string;
+ fqdn_message: string;
cache_prefix: string;
cache_icon: string;
complete_time: string;
diff --git a/hyperglass/ui/types/data.ts b/hyperglass/ui/types/data.ts
index e33d92c..87adbcf 100644
--- a/hyperglass/ui/types/data.ts
+++ b/hyperglass/ui/types/data.ts
@@ -15,6 +15,10 @@ export interface TFormState {
queryTarget: string;
}
+export interface TFormQuery extends Omit {
+ queryLocation: string;
+}
+
export interface TStringTableData extends Omit {
output: TStructuredResponse;
}
diff --git a/hyperglass/ui/types/table.ts b/hyperglass/ui/types/table.ts
index 0c9aa72..ff73652 100644
--- a/hyperglass/ui/types/table.ts
+++ b/hyperglass/ui/types/table.ts
@@ -1,6 +1,14 @@
+import type { CellProps } from 'react-table';
+
export interface TColumn {
Header: string;
accessor: keyof TRoute;
align: string;
hidden: boolean;
}
+
+export type TCellRender = {
+ column: CellProps['column'];
+ row: CellProps['row'];
+ value: CellProps['value'];
+};