1
0
Fork 1
mirror of https://github.com/thatmattlove/hyperglass.git synced 2026-01-17 08:48:05 +00:00

cleanup & code comments [skip ci]

This commit is contained in:
checktheroads 2020-12-31 23:09:54 -07:00
parent 486dd320ee
commit 4bbf5cde12
16 changed files with 197 additions and 321 deletions

View file

@ -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<boolean>({ base: true, md: true, lg: false, xl: false }) ?? true;
/**
* Convenience function to combine Chakra UI's useToken & useColorModeValue.
*/
export const useColorToken = <L extends string, D extends string>(
token: keyof ITheme,
light: L,

View file

@ -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';

View file

@ -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 extends unknown>(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 extends unknown | null>(s: S): S | null;
getResponse(d: string): TQueryResponse | null;
resolvedClose(): void;
resolvedOpen(): void;
formReady(): boolean;
resetForm(): void;
stateExporter<O extends unknown>(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;
};

View file

@ -9,6 +9,9 @@ async function query(ctx: TUseASNDetailFn): Promise<TASNDetails> {
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,

View file

@ -1,5 +1,8 @@
import { useMemo } from 'react';
/**
* Track the state of a boolean and return values based on its state.
*/
export function useBooleanValue<T extends any, F extends any>(
status: boolean,
ifTrue: T,

View file

@ -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<DnsOverHttps.Response | undefined> {
const [url, { target, family }] = ctx.queryKey;
@ -30,7 +33,20 @@ async function dnsQuery(ctx: TUseDNSQueryFn): Promise<DnsOverHttps.Response | un
return json;
}
export function useDNSQuery(target: string | null, family: 4 | 6) {
/**
* Query the configured DNS over HTTPS provider for the provided target. If `family` is `4`, only
* an A record will be queried. If `family` is `6`, only a AAAA record will be queried.
*/
export function useDNSQuery(
/**
* Hostname for DNS query.
*/
target: string | null,
/**
* Address family, e.g. IPv4 or IPv6.
*/
family: 4 | 6,
) {
const { cache, web } = useConfig();
return useQuery([web.dns_provider.url, { target, family }], dnsQuery, {
cacheTime: cache.timeout * 1000,

View file

@ -3,13 +3,19 @@ import { useConfig } from '~/context';
import { flatten } from '~/util';
import type { TDevice } from '~/types';
import type { TUseDevice } from './types';
export function useDevice(): (i: string) => 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<TDevice>(networks.map(n => n.locations)), []);
function getDevice(id: string): TDevice {
return devices.filter(dev => dev.name === id)[0];
}
return useCallback(getDevice, []);
}

View file

@ -7,6 +7,9 @@ import type { TUseGreetingReturn } from './types';
const ackState = createState<boolean>(false);
const openState = createState<boolean>(false);
/**
* Hook to manage the greeting, a.k.a. the popup at config path web.greeting.
*/
export function useGreeting(): TUseGreetingReturn {
const ack = useState<boolean>(ackState);
const isOpen = useState<boolean>(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;

View file

@ -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,
},
);

View file

@ -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<TLGState>) {
state.resolvedIsOpen.set(true);
}
/**
* Set the DNS resolver Popover to closed.
*/
public resolvedClose(state: State<TLGState>) {
state.resolvedIsOpen.set(false);
}
/**
* Find a response based on the device ID.
*/
public getResponse(state: State<TLGState>, 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<TLGState>): 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<TLGState>) {
state.merge({
queryVrf: '',
@ -62,13 +70,34 @@ class MethodsInstance {
selections: { queryLocation: [], queryType: null, queryVrf: null },
});
}
public 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;
}
}
/**
* Plugin Initialization.
*/
function Methods(): Plugin;
function Methods(inst: State<TLGState>): MethodsExtension;
function Methods(inst?: State<TLGState>): Plugin | MethodsExtension {
/**
* Plugin Attachment.
*/
function Methods(inst: State<TLGState>): TMethodsExtension;
/**
* Plugin Instance.
*/
function Methods(inst?: State<TLGState>): Plugin | TMethodsExtension {
if (inst) {
const [instance] = inst.attach(PluginID) as [
const [instance] = inst.attach(MethodsId) as [
MethodsInstance | Error,
PluginStateControl<TLGState>,
];
@ -83,46 +112,17 @@ function Methods(inst?: State<TLGState>): 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 extends unknown | null>(s: S): S | null;
getResponse(d: string): TQueryResponse | null;
resolvedClose(): void;
resolvedOpen(): void;
formReady(): boolean;
resetForm(): void;
};
const LGState = createState<TLGState>({
selections: { queryLocation: [], queryType: null, queryVrf: null },
resolvedIsOpen: false,
@ -138,27 +138,20 @@ const LGState = createState<TLGState>({
families: [],
});
/**
* Global state hook for state used throughout hyperglass.
*/
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;
}
/**
* 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);

View file

@ -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<string>('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]);
}

View file

@ -1,62 +0,0 @@
import { useEffect, useRef, useState } from 'react';
type ScaledTitleCallback = (f: string) => void;
function getWidthPx<R extends React.MutableRefObject<HTMLElement>>(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<HTMLDivElement>,
T extends React.MutableRefObject<HTMLHeadingElement>
>(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]);
}

View file

@ -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);
}

View file

@ -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 {

View file

@ -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 <Error msg="/structured" statusCode={404} />;
// }
return (
<>
<Head>

View file

@ -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 (
<Layout>
<Flex my={8} maxW={["100%", "100%", "75%", "75%"]} w="100%">
<BGPTable>{response.output}</BGPTable>
</Flex>
</Layout>
);
};
export default Structured;