From 4bbf5cde1272557a9bf6611a05f8fc1411fa0173 Mon Sep 17 00:00:00 2001 From: checktheroads Date: Thu, 31 Dec 2020 23:09:54 -0700 Subject: [PATCH] cleanup & code comments [skip ci] --- hyperglass/ui/context/HyperglassProvider.tsx | 15 +- hyperglass/ui/hooks/index.ts | 1 - hyperglass/ui/hooks/types.ts | 68 +++++++- hyperglass/ui/hooks/useASNDetail.ts | 3 + hyperglass/ui/hooks/useBooleanValue.ts | 3 + hyperglass/ui/hooks/useDNSQuery.ts | 18 ++- hyperglass/ui/hooks/useDevice.ts | 8 +- hyperglass/ui/hooks/useGreeting.ts | 6 + hyperglass/ui/hooks/useLGQuery.ts | 7 + hyperglass/ui/hooks/useLGState.ts | 111 ++++++------- hyperglass/ui/hooks/useOpposingColor.ts | 21 +-- hyperglass/ui/hooks/useScaledTitle.ts | 62 ------- hyperglass/ui/hooks/useStrf.ts | 7 +- hyperglass/ui/hooks/useTableToString.ts | 19 +-- hyperglass/ui/pages/_app.tsx | 7 +- hyperglass/ui/pages/structured.javascript | 162 ------------------- 16 files changed, 197 insertions(+), 321 deletions(-) delete mode 100644 hyperglass/ui/hooks/useScaledTitle.ts delete mode 100644 hyperglass/ui/pages/structured.javascript diff --git a/hyperglass/ui/context/HyperglassProvider.tsx b/hyperglass/ui/context/HyperglassProvider.tsx index 35be636..a9d3601 100644 --- a/hyperglass/ui/context/HyperglassProvider.tsx +++ b/hyperglass/ui/context/HyperglassProvider.tsx @@ -30,12 +30,25 @@ export const HyperglassProvider = (props: THyperglassProvider) => { ); }; +/** + * Get the current configuration. + */ export const useConfig = (): IConfig => useContext(HyperglassContext); + +/** + * Get the current theme object. + */ export const useTheme = (): ITheme => useChakraTheme(); +/** + * Determine if device is mobile or desktop based on Chakra UI theme breakpoints. + */ export const useMobile = (): boolean => - useBreakpointValue({ base: true, md: true, lg: false, xl: false }) ?? true; + useBreakpointValue({ base: true, md: true, lg: false, xl: false }) ?? true; +/** + * Convenience function to combine Chakra UI's useToken & useColorModeValue. + */ export const useColorToken = ( token: keyof ITheme, light: L, diff --git a/hyperglass/ui/hooks/index.ts b/hyperglass/ui/hooks/index.ts index eaff83a..a82290f 100644 --- a/hyperglass/ui/hooks/index.ts +++ b/hyperglass/ui/hooks/index.ts @@ -6,6 +6,5 @@ export * from './useGreeting'; export * from './useLGQuery'; export * from './useLGState'; export * from './useOpposingColor'; -export * from './useScaledTitle'; export * from './useStrf'; export * from './useTableToString'; diff --git a/hyperglass/ui/hooks/types.ts b/hyperglass/ui/hooks/types.ts index 8d1c4ac..853319d 100644 --- a/hyperglass/ui/hooks/types.ts +++ b/hyperglass/ui/hooks/types.ts @@ -1,6 +1,13 @@ import { State } from '@hookstate/core'; import type { QueryFunctionContext } from 'react-query'; -import type { TFormQuery } from '~/types'; +import type { + TDevice, + Families, + TFormQuery, + TDeviceVrf, + TQueryTypes, + TSelectOption, +} from '~/types'; export interface TOpposingOptions { light?: string; @@ -34,3 +41,62 @@ export interface TUseDNSQueryFn { pageParam?: QueryFunctionContext['pageParam']; queryKey: [string | null, TUseDNSQueryParams]; } + +export type TUseDevice = ( + /** + * Device's ID, e.g. the device.name field. + */ + deviceId: string, +) => TDevice; + +export interface TSelections { + queryLocation: TSelectOption[] | []; + queryType: TSelectOption | null; + queryVrf: TSelectOption | null; +} + +export interface TMethodsExtension { + getResponse(d: string): TQueryResponse | null; + resolvedClose(): void; + resolvedOpen(): void; + formReady(): boolean; + resetForm(): void; + stateExporter(o: O): O | null; +} + +export type TLGState = { + queryVrf: string; + families: Families; + queryTarget: string; + btnLoading: boolean; + isSubmitting: boolean; + displayTarget: string; + queryType: TQueryTypes; + queryLocation: string[]; + availVrfs: TDeviceVrf[]; + resolvedIsOpen: boolean; + selections: TSelections; + responses: { [d: string]: TQueryResponse }; +}; + +export type TLGStateHandlers = { + exportState(s: S): S | null; + getResponse(d: string): TQueryResponse | null; + resolvedClose(): void; + resolvedOpen(): void; + formReady(): boolean; + resetForm(): void; + stateExporter(o: O): O | null; +}; + +export type UseStrfArgs = { [k: string]: any } | string; + +export type TTableToStringFormatter = (v: any) => string; + +export type TTableToStringFormatted = { + age: (v: number) => string; + active: (v: boolean) => string; + as_path: (v: number[]) => string; + communities: (v: string[]) => string; + rpki_state: (v: number, n: TRPKIStates) => string; +}; diff --git a/hyperglass/ui/hooks/useASNDetail.ts b/hyperglass/ui/hooks/useASNDetail.ts index 807daba..5cddede 100644 --- a/hyperglass/ui/hooks/useASNDetail.ts +++ b/hyperglass/ui/hooks/useASNDetail.ts @@ -9,6 +9,9 @@ async function query(ctx: TUseASNDetailFn): Promise { return await res.json(); } +/** + * Query the bgpview.io API to get an ASN's organization name for the AS Path component. + */ export function useASNDetail(asn: string) { return useQuery(asn, query, { refetchOnWindowFocus: false, diff --git a/hyperglass/ui/hooks/useBooleanValue.ts b/hyperglass/ui/hooks/useBooleanValue.ts index 181543a..2922ce4 100644 --- a/hyperglass/ui/hooks/useBooleanValue.ts +++ b/hyperglass/ui/hooks/useBooleanValue.ts @@ -1,5 +1,8 @@ import { useMemo } from 'react'; +/** + * Track the state of a boolean and return values based on its state. + */ export function useBooleanValue( status: boolean, ifTrue: T, diff --git a/hyperglass/ui/hooks/useDNSQuery.ts b/hyperglass/ui/hooks/useDNSQuery.ts index 032b486..8f7d6c4 100644 --- a/hyperglass/ui/hooks/useDNSQuery.ts +++ b/hyperglass/ui/hooks/useDNSQuery.ts @@ -5,6 +5,9 @@ import { fetchWithTimeout } from '~/util'; import type { DnsOverHttps } from '~/types'; import type { TUseDNSQueryFn } from './types'; +/** + * Perform a DNS over HTTPS query using the application/dns-json MIME type. + */ async function dnsQuery(ctx: TUseDNSQueryFn): Promise { const [url, { target, family }] = ctx.queryKey; @@ -30,7 +33,20 @@ async function dnsQuery(ctx: TUseDNSQueryFn): Promise TDevice { +/** + * Get a device's configuration from the global configuration context based on its name. + */ +export function useDevice(): TUseDevice { const { networks } = useConfig(); + const devices = useMemo(() => flatten(networks.map(n => n.locations)), []); function getDevice(id: string): TDevice { return devices.filter(dev => dev.name === id)[0]; } + return useCallback(getDevice, []); } diff --git a/hyperglass/ui/hooks/useGreeting.ts b/hyperglass/ui/hooks/useGreeting.ts index ac5096d..ea25777 100644 --- a/hyperglass/ui/hooks/useGreeting.ts +++ b/hyperglass/ui/hooks/useGreeting.ts @@ -7,6 +7,9 @@ import type { TUseGreetingReturn } from './types'; const ackState = createState(false); const openState = createState(false); +/** + * Hook to manage the greeting, a.k.a. the popup at config path web.greeting. + */ export function useGreeting(): TUseGreetingReturn { const ack = useState(ackState); const isOpen = useState(openState); @@ -25,10 +28,13 @@ export function useGreeting(): TUseGreetingReturn { function greetingReady(): boolean { if (ack.get()) { + // If the acknowledgement is already set, no further evaluation is needed. return true; } else if (!web.greeting.required && !ack.get()) { + // If the acknowledgement is not set, but is also not required, then pass. return true; } else if (web.greeting.required && !ack.get()) { + // If the acknowledgement is not set, but is required, then fail. return false; } else { return false; diff --git a/hyperglass/ui/hooks/useLGQuery.ts b/hyperglass/ui/hooks/useLGQuery.ts index c394a46..6b9dd57 100644 --- a/hyperglass/ui/hooks/useLGQuery.ts +++ b/hyperglass/ui/hooks/useLGQuery.ts @@ -5,6 +5,9 @@ import { fetchWithTimeout } from '~/util'; import type { TFormQuery } from '~/types'; import type { TUseLGQueryFn } from './types'; +/** + * Custom hook handle submission of a query to the hyperglass backend. + */ export function useLGQuery(query: TFormQuery) { const { request_timeout, cache } = useConfig(); const controller = new AbortController(); @@ -34,9 +37,13 @@ export function useLGQuery(query: TFormQuery) { ['/api/query/', query], runQuery, { + // Invalidate react-query's cache just shy of the configured cache timeout. cacheTime: cache.timeout * 1000 * 0.95, + // Don't refetch when window refocuses. refetchOnWindowFocus: false, + // Don't automatically refetch query data (queries should be on-off). refetchInterval: false, + // Don't refetch on component remount. refetchOnMount: false, }, ); diff --git a/hyperglass/ui/hooks/useLGState.ts b/hyperglass/ui/hooks/useLGState.ts index 8a7f6ec..a7d12c8 100644 --- a/hyperglass/ui/hooks/useLGState.ts +++ b/hyperglass/ui/hooks/useLGState.ts @@ -4,28 +4,29 @@ import isEqual from 'react-fast-compare'; import { all } from '~/util'; import type { State, PluginStateControl, Plugin } from '@hookstate/core'; -import type { Families, TDeviceVrf, TQueryTypes, TSelectOption } from '~/types'; +import type { TLGState, TLGStateHandlers, TMethodsExtension } from './types'; -const PluginID = Symbol('Methods'); +const MethodsId = Symbol('Methods'); /** - * Public API + * hookstate plugin to provide convenience functions for the useLGState hook. */ -interface MethodsExtension { - getResponse(d: string): TQueryResponse | null; - resolvedClose(): void; - resolvedOpen(): void; - formReady(): boolean; - resetForm(): void; -} - class MethodsInstance { + /** + * Set the DNS resolver Popover to opened. + */ public resolvedOpen(state: State) { state.resolvedIsOpen.set(true); } + /** + * Set the DNS resolver Popover to closed. + */ public resolvedClose(state: State) { state.resolvedIsOpen.set(false); } + /** + * Find a response based on the device ID. + */ public getResponse(state: State, device: string): TQueryResponse | null { if (device in state.responses) { return state.responses[device].value; @@ -33,6 +34,10 @@ class MethodsInstance { return null; } } + /** + * Determine if the form is ready for submission, e.g. all fields have values and isSubmitting + * has been set to true. This ultimately controls the UI layout. + */ public formReady(state: State): boolean { return ( state.isSubmitting.value && @@ -46,6 +51,9 @@ class MethodsInstance { ) ); } + /** + * Reset form values affected by the form state to their default values. + */ public resetForm(state: State) { state.merge({ queryVrf: '', @@ -62,13 +70,34 @@ class MethodsInstance { selections: { queryLocation: [], queryType: null, queryVrf: null }, }); } + public stateExporter(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; + } } +/** + * Plugin Initialization. + */ function Methods(): Plugin; -function Methods(inst: State): MethodsExtension; -function Methods(inst?: State): Plugin | MethodsExtension { +/** + * Plugin Attachment. + */ +function Methods(inst: State): TMethodsExtension; +/** + * Plugin Instance. + */ +function Methods(inst?: State): Plugin | TMethodsExtension { if (inst) { - const [instance] = inst.attach(PluginID) as [ + const [instance] = inst.attach(MethodsId) as [ MethodsInstance | Error, PluginStateControl, ]; @@ -83,46 +112,17 @@ function Methods(inst?: State): Plugin | MethodsExtension { resolvedOpen: () => instance.resolvedOpen(inst), resolvedClose: () => instance.resolvedClose(inst), getResponse: device => instance.getResponse(inst, device), + stateExporter: obj => instance.stateExporter(obj), }; } return { - id: PluginID, + id: MethodsId, 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; - isSubmitting: boolean; - displayTarget: string; - queryType: TQueryTypes; - queryLocation: string[]; - availVrfs: TDeviceVrf[]; - resolvedIsOpen: boolean; - selections: TSelections; - responses: { [d: string]: TQueryResponse }; -}; - -type TLGStateHandlers = { - exportState(s: S): S | null; - getResponse(d: string): TQueryResponse | null; - resolvedClose(): void; - resolvedOpen(): void; - formReady(): boolean; - resetForm(): void; -}; - const LGState = createState({ selections: { queryLocation: [], queryType: null, queryVrf: null }, resolvedIsOpen: false, @@ -138,27 +138,20 @@ const LGState = createState({ families: [], }); +/** + * Global state hook for state used throughout hyperglass. + */ export function useLGState(): State { return useState(LGState); } -function stateExporter(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; -} - +/** + * Plugin for useLGState() that provides convenience methods for its state. + */ export function useLGMethods(): TLGStateHandlers { const state = useLGState(); state.attach(Methods); - const exporter = useCallback(stateExporter, [isEqual]); + const exporter = useCallback(Methods(state).stateExporter, [isEqual]); return { exportState(s) { return exporter(s); diff --git a/hyperglass/ui/hooks/useOpposingColor.ts b/hyperglass/ui/hooks/useOpposingColor.ts index df8c867..ade9d92 100644 --- a/hyperglass/ui/hooks/useOpposingColor.ts +++ b/hyperglass/ui/hooks/useOpposingColor.ts @@ -1,10 +1,13 @@ -import { useMemo, useState } from 'react'; -import { useToken } from '@chakra-ui/react'; +import { useMemo } from 'react'; import { getColor, isLight } from '@chakra-ui/theme-tools'; import { useTheme } from '~/context'; import type { TOpposingOptions } from './types'; +/** + * Parse the color string to determine if it's a Chakra UI theme key, and determine if the + * opposing color should be black or white. + */ export function useIsDark(color: string) { const theme = useTheme(); if (typeof color === 'string' && color.match(/[a-zA-Z]+\.[a-zA-Z0-9]+/g)) { @@ -19,6 +22,9 @@ export function useIsDark(color: string) { return opposingShouldBeDark; } +/** + * Determine if the foreground color for `color` should be white or black. + */ export function useOpposingColor(color: string, options?: TOpposingOptions): string { const isBlack = useIsDark(color); @@ -30,14 +36,3 @@ export function useOpposingColor(color: string, options?: TOpposingOptions): str } }, [color]); } - -export function useOpposingToken(color: string, options?: TOpposingOptions): string { - const [opposingColor, setOpposingColor] = useState('inherit'); - const isBlack = useIsDark(color); - const dark = options?.dark ?? 'dark'; - const light = options?.light ?? 'light'; - - isBlack && opposingColor !== dark && setOpposingColor(dark); - !isBlack && opposingColor !== light && setOpposingColor(light); - return useMemo(() => opposingColor, [color]); -} diff --git a/hyperglass/ui/hooks/useScaledTitle.ts b/hyperglass/ui/hooks/useScaledTitle.ts deleted file mode 100644 index f65aabb..0000000 --- a/hyperglass/ui/hooks/useScaledTitle.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -type ScaledTitleCallback = (f: string) => void; - -function getWidthPx>(ref: R) { - const computedStyle = window.getComputedStyle(ref.current); - const widthStr = computedStyle.width.replaceAll('px', ''); - const width = parseFloat(widthStr); - return width; -} - -function reducePx(px: number) { - return px * 0.9; -} - -function reducer(val: number, tooBig: () => boolean): number { - let r = val; - if (tooBig()) { - r = reducePx(val); - } - return r; -} - -/** - * - * useScaledTitle( - * f => { - * setFontsize(f); - * }, - * titleRef, - * ref, - * [showSubtitle], - * ); - */ -export function useScaledTitle< - P extends React.MutableRefObject, - T extends React.MutableRefObject ->(callback: ScaledTitleCallback, parentRef: P, titleRef: T, deps: any[] = []) { - console.log(deps); - const [fontSize, setFontSize] = useState(''); - const calcSize = useRef(0); - - function effect() { - const computedSize = window.getComputedStyle(titleRef.current).getPropertyValue('font-size'); - - const fontPx = parseFloat(computedSize.replaceAll('px', '')); - calcSize.current = fontPx; - - if (typeof window !== 'undefined') { - calcSize.current = reducer( - calcSize.current, - () => getWidthPx(titleRef) >= getWidthPx(parentRef), - ); - - setFontSize(`${calcSize.current}px`); - - return callback(fontSize); - } - } - - return useEffect(effect, [...deps, callback]); -} diff --git a/hyperglass/ui/hooks/useStrf.ts b/hyperglass/ui/hooks/useStrf.ts index 2edf435..758e6ae 100644 --- a/hyperglass/ui/hooks/useStrf.ts +++ b/hyperglass/ui/hooks/useStrf.ts @@ -1,8 +1,11 @@ import { useMemo } from 'react'; import format from 'string-format'; -type FmtArgs = { [k: string]: any } | string; +import type { UseStrfArgs } from './types'; -export function useStrf(str: string, fmt: FmtArgs, ...deps: any[]): string { +/** + * Format a string with variables, like Python's string.format() + */ +export function useStrf(str: string, fmt: UseStrfArgs, ...deps: any[]): string { return useMemo(() => format(str, fmt), deps); } diff --git a/hyperglass/ui/hooks/useTableToString.ts b/hyperglass/ui/hooks/useTableToString.ts index 5c3e40b..7ddddd9 100644 --- a/hyperglass/ui/hooks/useTableToString.ts +++ b/hyperglass/ui/hooks/useTableToString.ts @@ -5,19 +5,11 @@ import utcPlugin from 'dayjs/plugin/utc'; import { useConfig } from '~/context'; import { isStructuredOutput } from '~/types'; +import type { TTableToStringFormatter, TTableToStringFormatted } from './types'; + dayjs.extend(relativeTimePlugin); dayjs.extend(utcPlugin); -type TFormatter = (v: any) => string; - -type TFormatted = { - age: (v: number) => string; - active: (v: boolean) => string; - as_path: (v: number[]) => string; - communities: (v: string[]) => string; - rpki_state: (v: number, n: TRPKIStates) => string; -}; - function formatAsPath(path: number[]): string { return path.join(' → '); } @@ -45,6 +37,9 @@ function formatTime(val: number): string { return `${relative} (${timestamp})`; } +/** + * Get a function to convert table data to string, for use in the copy button component. + */ export function useTableToString( target: string, data: TQueryResponse | undefined, @@ -70,11 +65,11 @@ export function useTableToString( rpki_state: formatRpkiState, }; - function isFormatted(key: string): key is keyof TFormatted { + function isFormatted(key: string): key is keyof TTableToStringFormatted { return key in tableFormatMap; } - function getFmtFunc(accessor: keyof TRoute): TFormatter { + function getFmtFunc(accessor: keyof TRoute): TTableToStringFormatter { if (isFormatted(accessor)) { return tableFormatMap[accessor]; } else { diff --git a/hyperglass/ui/pages/_app.tsx b/hyperglass/ui/pages/_app.tsx index 2221827..9edd33a 100644 --- a/hyperglass/ui/pages/_app.tsx +++ b/hyperglass/ui/pages/_app.tsx @@ -1,8 +1,7 @@ import Head from 'next/head'; import { HyperglassProvider } from '~/context'; import { IConfig } from '~/types'; -// import { useRouter } from "next/router"; -// import Error from "./_error"; + if (process.env.NODE_ENV === 'development') { require('@hookstate/devtools'); } @@ -21,10 +20,6 @@ const App = (props: TApp) => { const { Component, pageProps, appProps } = props; const { config } = appProps; - // const { asPath } = useRouter(); - // if (asPath === "/structured") { - // return ; - // } return ( <> diff --git a/hyperglass/ui/pages/structured.javascript b/hyperglass/ui/pages/structured.javascript deleted file mode 100644 index 33d1219..0000000 --- a/hyperglass/ui/pages/structured.javascript +++ /dev/null @@ -1,162 +0,0 @@ -import * as React from "react"; -import { Flex } from "@chakra-ui/core"; -import {BGPTable,Layout} from "app/components"; - -const response = { - cached: false, - format: "application/json", - keywords: [], - level: "success", - output: { - count: 5, - routes: [ - { - active: true, - age: 1310798, - as_path: [1299, 13335], - communities: [ - "1299:35000", - "14525:0", - "14525:40", - "14525:1021", - "14525:2840", - "14525:3001", - "14525:4001", - "14525:9003" - ], - local_preference: 150, - med: 0, - next_hop: "62.115.189.136", - peer_rid: "2.255.254.51", - prefix: "1.1.1.0/24", - rpki_state: 3, - source_as: 13335, - source_rid: "162.158.140.1", - weight: 170 - }, - { - active: false, - age: 1310792, - as_path: [174, 13335], - communities: [ - "174:21001", - "174:22013", - "14525:0", - "14525:20", - "14525:1021", - "14525:2840", - "14525:3001", - "14525:4001", - "14525:9001" - ], - local_preference: 150, - med: 2020, - next_hop: "100.64.0.122", - peer_rid: "199.34.92.1", - prefix: "1.1.1.0/24", - rpki_state: 3, - source_as: 13335, - source_rid: "162.158.140.1", - weight: 170 - }, - { - active: false, - age: 70883, - as_path: [13335], - communities: [ - "13335:10232", - "13335:19000", - "13335:20050", - "13335:20500", - "13335:20530", - "14525:0", - "14525:20", - "14525:1021", - "14525:2840", - "14525:3002", - "14525:4003", - "14525:9009" - ], - local_preference: 250, - med: 0, - next_hop: "100.64.0.122", - peer_rid: "199.34.92.5", - prefix: "1.1.1.0/24", - rpki_state: 3, - source_as: 13335, - source_rid: "172.68.129.1", - weight: 200 - }, - { - active: false, - age: 70862, - as_path: [13335], - communities: [ - "13335:10232", - "13335:19000", - "13335:20050", - "13335:20500", - "13335:20530", - "14525:0", - "14525:20", - "14525:1021", - "14525:2840", - "14525:3002", - "14525:4003", - "14525:9009" - ], - local_preference: 250, - med: 0, - next_hop: "100.64.0.122", - peer_rid: "199.34.92.6", - prefix: "1.1.1.0/24", - rpki_state: 3, - source_as: 13335, - source_rid: "172.68.129.1", - weight: 200 - }, - { - active: false, - age: 1124791, - as_path: [174, 13335], - communities: [ - "174:21001", - "174:22003", - "14525:0", - "14525:40", - "14525:1021", - "14525:2840", - "14525:3003", - "14525:4004", - "14525:9001" - ], - local_preference: 150, - med: 25090, - next_hop: "100.64.0.122", - peer_rid: "199.34.92.7", - prefix: "1.1.1.0/24", - rpki_state: 3, - source_as: 13335, - source_rid: "108.162.239.1", - weight: 200 - } - ], - vrf: "default", - winning_weight: "low" - }, - random: "60d6663342e1c1e3e1b2a6259b22023b45e0568dd7e31aeee9c453cf6e7091d5", - runtime: 5, - timestamp: "2020-06-06 04:38:46" -}; - -const Structured = () => { - return ( - - - {response.output} - - - ); -}; - -export default Structured;