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

Update next, eslint, fix lint errors

This commit is contained in:
thatmattlove 2021-09-08 01:02:25 -07:00
parent 5ccfe50792
commit eff99ad294
46 changed files with 1051 additions and 1189 deletions

View file

@ -2,33 +2,26 @@
UI_DIR="$(pwd)/hyperglass/ui"
check_typescript () {
cd $UI_DIR
node_modules/.bin/tsc --noEmit
check_typescript() {
yarn --cwd $UI_DIR typecheck
}
check_eslint () {
cd $UI_DIR
node_modules/.bin/eslint . --ext .ts --ext .tsx
check_eslint() {
yarn --cwd $UI_DIR lint
}
check_prettier () {
cd $UI_DIR
node_modules/.bin/prettier -c .
check_prettier() {
yarn --cwd $UI_DIR prettier -c .
}
for arg in "$@"
do
if [ "$arg" == "--typescript" ]
then
for arg in "$@"; do
if [ "$arg" == "--typescript" ]; then
check_typescript
exit $?
elif [ "$arg" == "--eslint" ]
then
elif [ "$arg" == "--eslint" ]; then
check_eslint
exit $?
elif [ "$arg" == "--prettier" ]
then
elif [ "$arg" == "--prettier" ]; then
check_prettier
exit $?
else

13
hyperglass/TODO.md Normal file
View file

@ -0,0 +1,13 @@
# Left off: 2021 09 05
Implemented validation, seems to work.
Disabled Scrapli for Juniper, need to figure out WTF is going on there, possibly remove.
## Next
- [x] Figure out le/ge policy in validation
- [x] Select options doesn't work in UI - it's just a text field
- [x] Query result labels are broken
- [ ] `KeyError: 'device_name'` when raising a device connection error
- [ ] Fix VRF/query group label in UI
- [ ] Selecting multiple sites without overlapping query groups marks the field with a red outline, but no error
- [ ] Look at replacing location dropdown with something more snazzy
- [ ] See todos for circular imports

View file

@ -1,60 +0,0 @@
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:prettier/recommended",
"prettier/@typescript-eslint"
],
"parser": "@typescript-eslint/parser",
"env": {
"browser": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"arrowFunctions": true
},
"project": "./tsconfig.json"
},
"plugins": ["react", "@typescript-eslint", "prettier"],
"settings": {
"react": {
"version": "detect"
},
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"],
"paths": ["./src"]
}
}
},
"rules": {
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-unused-vars-experimental": "error",
"no-unused-vars": "off",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"comma-dangle": ["error", "always-multiline"],
"global-require": "off",
"import/no-dynamic-require": "off",
"import/prefer-default-export": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-empty-interface": [
"error",
{
"allowSingleExtends": true
}
]
}
}

View file

@ -0,0 +1,69 @@
module.exports = {
root: true,
extends: ['eslint:recommended'],
env: {
es6: true,
node: true,
},
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
arrowFunctions: true,
},
project: './tsconfig.json',
},
overrides: [
{
files: ['**/*.ts', '**/*.tsx'],
parser: '@typescript-eslint/parser',
settings: {
react: { version: 'detect' },
'import/resolver': {
typescript: {},
},
},
env: {
browser: true,
node: true,
es6: true,
},
extends: [
'eslint:recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:prettier/recommended',
],
rules: {
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars-experimental': 'error',
'no-unused-vars': 'off',
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'comma-dangle': ['error', 'always-multiline'],
'global-require': 'off',
'import/no-dynamic-require': 'off',
'import/prefer-default-export': 'off',
'import/no-named-as-default-member': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true,
},
],
},
},
],
};

View file

@ -24,8 +24,9 @@ export const FooterButton: React.FC<TFooterButton> = (props: TFooterButton) => {
const { content, title, side, ...rest } = props;
const config = useConfig();
const fmt = useMemo(() => getConfigFmt(config), []);
const fmtContent = useStrf(content, fmt);
const strF = useStrf();
const fmt = useMemo(() => getConfigFmt(config), [config]);
const fmtContent = useMemo(() => strF(content, fmt), [fmt, content, strF]);
const placement = side === 'left' ? 'top' : side === 'right' ? 'top-end' : undefined;
const bg = useColorValue('white', 'gray.900');

View file

@ -36,7 +36,9 @@ export const Footer: React.FC = () => {
const isMobile = useMobile();
const [left, right] = useMemo(() => buildItems(web.links, web.menus), []);
const [left, right] = useMemo(() => buildItems(web.links, web.menus), [web.links, web.menus]);
const strF = useStrf();
return (
<HStack
@ -55,7 +57,7 @@ export const Footer: React.FC = () => {
>
{left.map(item => {
if (isLink(item)) {
const url = useStrf(item.url, { primary_asn }) ?? '/';
const url = strF(item.url, { primary_asn }, '/');
const icon: Partial<ButtonProps & LinkProps> = {};
if (item.show_icon) {
@ -71,7 +73,7 @@ export const Footer: React.FC = () => {
{!isMobile && <Flex p={0} flex="1 0 auto" maxWidth="100%" mr="auto" />}
{right.map(item => {
if (isLink(item)) {
const url = useStrf(item.url, { primary_asn }) ?? '/';
const url = strF(item.url, { primary_asn }, '/');
const icon: Partial<ButtonProps & LinkProps> = {};
if (item.show_icon) {

View file

@ -26,7 +26,7 @@ export const FormField: React.FC<TField> = (props: TField) => {
setError(errors[name]);
console.warn(`Error on field '${label}': ${error?.message}`);
}
}, [error, errors, setError]);
}, [error, errors, label, name, setError]);
return (
<FormControl

View file

@ -12,7 +12,8 @@ export const QueryGroup: React.FC<TQueryGroup> = (props: TQueryGroup) => {
const options = useMemo<TSelectOption[]>(
() => availableGroups.map(g => ({ label: g.value, value: g.value })),
[availableGroups.length, queryLocation.length],
// eslint-disable-next-line react-hooks/exhaustive-deps
[availableGroups, queryLocation],
);
function handleChange(e: TSelectOption | TSelectOption[]): void {

View file

@ -33,7 +33,7 @@ export const QueryLocation: React.FC<TQuerySelectField> = (props: TQuerySelectFi
const { selections } = useLGState();
const { exportState } = useLGMethods();
const options = useMemo(() => buildOptions(networks), [networks.length]);
const options = useMemo(() => buildOptions(networks), [networks]);
function handleChange(e: TSelectOption | TSelectOption[]): void {
if (e === null) {

View file

@ -45,7 +45,7 @@ export const QueryTarget: React.FC<TQueryTarget> = (props: TQueryTarget) => {
const { queryTarget, displayTarget } = useLGState();
const directive = useDirective();
const options = useMemo(() => buildOptions(directive), [directive, buildOptions]);
const options = useMemo(() => buildOptions(directive), [directive]);
function handleInputChange(e: React.ChangeEvent<HTMLInputElement>): void {
displayTarget.set(e.target.value);

View file

@ -16,7 +16,7 @@ export const QueryType: React.FC<TQuerySelectField> = (props: TQuerySelectField)
const options = useMemo(
() => availableTypes.map(t => ({ label: t.name.value, value: t.id.value })),
[availableTypes.length],
[availableTypes],
);
function handleChange(e: TSelectOption | TSelectOption[]): void {

View file

@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import dynamic from 'next/dynamic';
import { Button, chakra, Stack, Text, VStack } from '@chakra-ui/react';
import { useConfig, useColorValue } from '~/context';
@ -26,6 +26,7 @@ function findAnswer(data: DnsOverHttps.Response | undefined): string {
export const ResolvedTarget: React.FC<TResolvedTarget> = (props: TResolvedTarget) => {
const { setTarget, errorClose } = props;
const strF = useStrf();
const { web } = useConfig();
const { displayTarget, isSubmitting, families, queryTarget } = useLGState();
@ -35,21 +36,25 @@ export const ResolvedTarget: React.FC<TResolvedTarget> = (props: TResolvedTarget
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 tooltip4 = strF(web.text.fqdn_tooltip, { protocol: 'IPv4' });
const tooltip6 = strF(web.text.fqdn_tooltip, { protocol: 'IPv6' });
const [messageStart, messageEnd] = web.text.fqdn_message.split('{fqdn}');
const [errorStart, errorEnd] = web.text.fqdn_error.split('{fqdn}');
const { data: data4, isLoading: isLoading4, isError: isError4, error: error4 } = useDNSQuery(
displayTarget.value,
4,
);
const {
data: data4,
isLoading: isLoading4,
isError: isError4,
error: error4,
} = useDNSQuery(displayTarget.value, 4);
const { data: data6, isLoading: isLoading6, isError: isError6, error: error6 } = useDNSQuery(
displayTarget.value,
6,
);
const {
data: data6,
isLoading: isLoading6,
isError: isError6,
error: error6,
} = useDNSQuery(displayTarget.value, 6);
isError4 && console.error(error4);
isError6 && console.error(error6);
@ -57,9 +62,11 @@ export const ResolvedTarget: React.FC<TResolvedTarget> = (props: TResolvedTarget
const answer4 = useMemo(() => findAnswer(data4), [data4]);
const answer6 = useMemo(() => findAnswer(data6), [data6]);
function handleOverride(value: string): void {
setTarget({ field: 'query_target', value });
}
const handleOverride = useCallback(
(value: string): void => setTarget({ field: 'query_target', value }),
[setTarget],
);
function selectTarget(value: string): void {
queryTarget.set(value);
isSubmitting.set(true);
@ -73,7 +80,7 @@ export const ResolvedTarget: React.FC<TResolvedTarget> = (props: TResolvedTarget
} else if (query4 && data4?.Answer) {
handleOverride(findAnswer(data4));
}
}, [data4, data6]);
}, [data4, data6, handleOverride, query4, query6]);
return (
<VStack w="100%" spacing={4} justify="center">

View file

@ -1,6 +1,6 @@
import { Flex } from '@chakra-ui/react';
import { FlexProps } from '@chakra-ui/react';
import type { FlexProps } from '@chakra-ui/react';
export const FormRow: React.FC<FlexProps> = (props: FlexProps) => {
return (

View file

@ -39,7 +39,7 @@ export const Greeting: React.FC<TGreeting> = (props: TGreeting) => {
if (!greetingAck.value && web.greeting.enable) {
isOpen.set(true);
}
}, []);
}, [greetingAck.value, isOpen, web.greeting.enable]);
return (
<Modal
size="lg"

View file

@ -1,6 +1,6 @@
import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { Image, Skeleton } from '@chakra-ui/react';
import { useColorValue, useConfig, useColorMode } from '~/context';
import { useColorValue, useConfig } from '~/context';
import type { TLogo } from './types';
@ -10,7 +10,6 @@ import type { TLogo } from './types';
function useLogo(): [string, () => void] {
const { web } = useConfig();
const { dark_format, light_format } = web.logo;
const { colorMode } = useColorMode();
const src = useColorValue(`/images/dark${dark_format}`, `/images/light${light_format}`);
@ -22,16 +21,14 @@ function useLogo(): [string, () => void] {
const [fallback, setSource] = useState<string | null>(null);
/**
* If the user image cannot be loaded, log an error to the console and set the fallback image.
*/
function setFallback() {
// If the user image cannot be loaded, log an error to the console and set the fallback image.
const setFallback = useCallback(() => {
console.warn(`Error loading image from '${src}'`);
setSource(fallbackSrc);
}
}, [fallbackSrc, src]);
// Only return the fallback image if it's been set.
return useMemo(() => [fallback ?? src, setFallback], [colorMode]);
return useMemo(() => [fallback ?? src, setFallback], [fallback, setFallback, src]);
}
export const Logo: React.FC<TLogo> = (props: TLogo) => {

View file

@ -55,5 +55,6 @@ export function useTitleSize(title: string, defaultSize: Sizes, deps: unknown[]
return useMemo(() => {
getSize(title.length);
return realSize;
}, [title, isMobile, ...deps]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [title, isMobile, realSize, ...deps]);
}

View file

@ -6,7 +6,6 @@ import { Frame } from './frame';
export const Layout: React.FC = () => {
const { formReady } = useLGMethods();
const ready = formReady();
console.log('ready', ready);
return (
<Frame>
{ready ? (

View file

@ -48,10 +48,11 @@ export const LookingGlass: React.FC = () => {
const { ack, greetingReady } = useGreeting();
const getDevice = useDevice();
const strF = useStrf();
const noQueryType = useStrf(messages.no_input, { field: web.text.query_type });
const noQueryLoc = useStrf(messages.no_input, { field: web.text.query_location });
const noQueryTarget = useStrf(messages.no_input, { field: web.text.query_target });
const noQueryType = strF(messages.no_input, { field: web.text.query_type });
const noQueryLoc = strF(messages.no_input, { field: web.text.query_location });
const noQueryTarget = strF(messages.no_input, { field: web.text.query_target });
const {
availableGroups,
@ -68,7 +69,7 @@ export const LookingGlass: React.FC = () => {
selections,
} = useLGState();
const queryTypes = useMemo(() => availableTypes.map(t => t.id.value), [availableTypes.length]);
const queryTypes = useMemo(() => availableTypes.map(t => t.id.value), [availableTypes]);
const formSchema = vest.create((data: TFormData = {} as TFormData) => {
test('query_location', noQueryLoc, () => {
@ -111,7 +112,8 @@ export const LookingGlass: React.FC = () => {
return directive;
}
return null;
}, [queryType.value, queryGroup.value]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryType.value, queryGroup.value, getDirective]);
function submitHandler() {
console.table({

View file

@ -48,7 +48,7 @@ function hasNode<C>(p: any): p is C & MDProps {
function clean<P extends ChakraProps>(props: P): P {
if (hasNode<P>(props)) {
const { node, ...rest } = props;
const r = (rest as unknown) as P;
const r = rest as unknown as P;
return r;
}
return props;

View file

@ -31,14 +31,14 @@ export const Meta: React.FC = () => {
} = useConfig();
const siteName = `${title} - ${description}`;
const primaryFont = useMemo(() => googleFontUrl(fonts.body), []);
const monoFont = useMemo(() => googleFontUrl(fonts.mono), []);
const primaryFont = useMemo(() => googleFontUrl(fonts.body), [fonts.body]);
const monoFont = useMemo(() => googleFontUrl(fonts.mono), [fonts.mono]);
useEffect(() => {
if (typeof window !== 'undefined' && location === '/') {
setLocation(window.location.href);
}
}, []);
}, [location]);
return (
<Head>

View file

@ -1,7 +1,5 @@
import { Box, Flex, SkeletonText, Badge, VStack } from '@chakra-ui/react';
import ReactFlow from 'react-flow-renderer';
import { Background, ReactFlowProvider } from 'react-flow-renderer';
import { Handle, Position } from 'react-flow-renderer';
import ReactFlow, { Background, ReactFlowProvider, Handle, Position } from 'react-flow-renderer';
import { useConfig, useColorValue, useColorToken } from '~/context';
import { useASNDetail } from '~/hooks';
import { Controls } from './controls';

View file

@ -11,7 +11,7 @@ const NODE_HEIGHT = 48;
export function useElements(base: BasePath, data: TStructuredResponse): FlowElement[] {
return useMemo(() => {
return [...buildElements(base, data)];
}, [data.routes.length]);
}, [base, data]);
}
/**

View file

@ -23,8 +23,9 @@ export const ResultHeader: React.FC<TResultHeader> = (props: TResultHeader) => {
const defaultStatus = useColorValue('success.500', 'success.300');
const { web } = useConfig();
const text = useStrf(web.text.complete_time, { seconds: runtime }, [runtime]);
const label = useMemo(() => runtimeText(runtime, text), [runtime]);
const strF = useStrf();
const text = strF(web.text.complete_time, { seconds: runtime });
const label = useMemo(() => runtimeText(runtime, text), [runtime, text]);
const color = useOpposingColor(isError ? warning : defaultStatus);

View file

@ -52,7 +52,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
const { responses } = useLGState();
const { data, error, isError, isLoading, refetch, isFetching, isFetchedAfterMount } = useLGQuery({
const { data, error, isError, isLoading, refetch, isFetchedAfterMount } = useLGQuery({
queryLocation,
queryTarget,
queryType,
@ -60,17 +60,13 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
queryGroup,
});
const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [
data,
isLoading,
isFetching,
]);
const isCached = useMemo(() => data?.cached || !isFetchedAfterMount, [data, isFetchedAfterMount]);
if (typeof data !== 'undefined') {
responses.merge({ [device._id]: data });
}
const cacheLabel = useStrf(web.text.cache_icon, { time: data?.timestamp }, [data?.timestamp]);
const strF = useStrf();
const cacheLabel = strF(web.text.cache_icon, { time: data?.timestamp });
const errorKeywords = useMemo(() => {
let kw = [] as string[];
@ -95,7 +91,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
} else {
return messages.general;
}
}, [isError, error, data]);
}, [error, data, messages.general, messages.request_timeout]);
isError && console.error(error);
@ -114,7 +110,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
e = statusMap[idx];
}
return e;
}, [isError, isLoading, data]);
}, [error]);
const tableComponent = useMemo<boolean>(() => {
let result = false;
@ -145,7 +141,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
setIndex([index]);
}
}
}, [data, isError]);
}, [data, index, indices, isLoading, isError, setIndex]);
return (
<AnimatedAccordionItem
@ -165,7 +161,6 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
<AccordionHeaderWrapper>
<AccordionButton py={2} w="unset" _hover={{}} _focus={{}} flex="1 0 auto">
<ResultHeader
// isError={isLGOutputOrError(data)}
isError={isError}
loading={isLoading}
errorMsg={errorMsg}

View file

@ -23,7 +23,8 @@ export const Tags: React.FC = () => {
return directive;
}
return null;
}, [queryType.value, queryGroup.value]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryType.value, queryGroup.value, getDirective]);
const targetBg = useToken('colors', 'teal.600');
const queryBg = useToken('colors', 'cyan.500');

View file

@ -82,7 +82,7 @@ export function useResults(initial: TUseResults['locations']): UseResultsReturn
if (resultsState.firstOpen.value === null && resultsState.locations.keys.length === 0) {
resultsState.set({ firstOpen: null, locations: initial });
}
}, []);
}, [initial]);
const results = useState(resultsState);
results.attach(Methods as () => Plugin);
@ -94,7 +94,7 @@ export function useResults(initial: TUseResults['locations']): UseResultsReturn
return () => {
results.set(initialState);
};
}, []);
}, [results]);
return { results, ...methods };
}

View file

@ -31,11 +31,10 @@ export const Select: React.FC<TSelectBase> = (props: TSelectBase) => {
const { colorMode } = useColorMode();
const selectContext = useMemo<TSelectContext>(() => ({ colorMode, isOpen, isError }), [
colorMode,
isError,
isOpen,
]);
const selectContext = useMemo<TSelectContext>(
() => ({ colorMode, isOpen, isError }),
[colorMode, isError, isOpen],
);
const defaultOnChange = (changed: TSelectOption | TSelectOption[]) => {
if (!Array.isArray(changed)) {

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useCallback, useMemo } from 'react';
import { useToken } from '@chakra-ui/react';
import { mergeWith } from '@chakra-ui/utils';
@ -114,13 +115,10 @@ export const useOptionStyle = (base: TStyles, state: TOption): TStyles => {
fontSize,
};
return useMemo(() => mergeWith({}, base, styles), [
isOpen,
colorMode,
isFocused,
isDisabled,
isSelected,
]);
return useMemo(
() => mergeWith({}, base, styles),
[isOpen, colorMode, isFocused, isDisabled, isSelected],
);
};
export const useIndicatorSeparatorStyle = (base: TStyles): TStyles => {

View file

@ -3,6 +3,5 @@ export * from './button';
export * from './cell';
export * from './head';
export * from './main';
export * from './main';
export * from './pageSelect';
export * from './row';

View file

@ -1,5 +1,6 @@
import { Select } from '@chakra-ui/react';
import { SelectProps } from '@chakra-ui/react';
import type { SelectProps } from '@chakra-ui/react';
export const TableSelectShow: React.FC<SelectProps> = (props: SelectProps) => {
const { value, ...rest } = props;

View file

@ -18,7 +18,7 @@ const queryClient = new QueryClient();
export const HyperglassProvider: React.FC<THyperglassProvider> = (props: THyperglassProvider) => {
const { config, children } = props;
const value = useMemo(() => config, []);
const value = useMemo(() => config, [config]);
const userTheme = value && makeTheme(value.web.theme, value.web.theme.default_color_mode);
const theme = value ? userTheme : defaultTheme;
return (

View file

@ -14,5 +14,5 @@ export function useBooleanValue<T extends unknown, F extends unknown>(
} else {
return ifFalse;
}
}, [status]);
}, [status, ifTrue, ifFalse]);
}

View file

@ -10,11 +10,11 @@ import type { TUseDevice } from './types';
export function useDevice(): TUseDevice {
const { networks } = useConfig();
const devices = useMemo(() => networks.map(n => n.locations).flat(), []);
const devices = useMemo(() => networks.map(n => n.locations).flat(), [networks]);
function getDevice(id: string): TDevice {
return devices.filter(dev => dev._id === id)[0];
}
return useCallback(getDevice, []);
return useCallback(getDevice, [devices]);
}

View file

@ -16,5 +16,6 @@ export function useDirective(): Nullable<TDirective> {
return directive.value;
}
return null;
}, [queryType.value, queryGroup.value]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryType.value, queryGroup.value, getDirective]);
}

View file

@ -9,73 +9,88 @@ const enabledState = createState<boolean>(false);
export function useGoogleAnalytics(): GAReturn {
const enabled = useState<boolean>(enabledState);
const useAnalytics = useCallback((effect: GAEffect): void => {
if (typeof window !== 'undefined' && enabled.value) {
if (typeof effect === 'function') {
effect(ReactGA);
const runEffect = useCallback(
(effect: GAEffect): void => {
if (typeof window !== 'undefined' && enabled.value) {
if (typeof effect === 'function') {
effect(ReactGA);
}
}
}
}, []);
},
[enabled.value],
);
const trackEvent = useCallback((e: ReactGA.EventArgs) => {
useAnalytics(ga => {
if (process.env.NODE_ENV === 'production') {
ga.event(e);
} else {
console.log(
`%cEvent %c${JSON.stringify(e)}`,
'background: green; color: black; padding: 0.5rem; font-size: 0.75rem;',
'background: black; color: green; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
);
const trackEvent = useCallback(
(e: ReactGA.EventArgs) => {
runEffect(ga => {
if (process.env.NODE_ENV === 'production') {
ga.event(e);
} else {
console.log(
`%cEvent %c${JSON.stringify(e)}`,
'background: green; color: black; padding: 0.5rem; font-size: 0.75rem;',
'background: black; color: green; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
);
}
});
},
[runEffect],
);
const trackPage = useCallback(
(path: string) => {
runEffect(ga => {
if (process.env.NODE_ENV === 'production') {
ga.pageview(path);
} else {
console.log(
`%cPage View %c${path}`,
'background: blue; color: white; padding: 0.5rem; font-size: 0.75rem;',
'background: white; color: blue; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
);
}
});
},
[runEffect],
);
const trackModal = useCallback(
(path: string) => {
runEffect(ga => {
if (process.env.NODE_ENV === 'production') {
ga.modalview(path);
} else {
console.log(
`%cModal View %c${path}`,
'background: red; color: white; padding: 0.5rem; font-size: 0.75rem;',
'background: white; color: red; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
);
}
});
},
[runEffect],
);
const initialize = useCallback(
(trackingId: string, debug: boolean) => {
if (typeof trackingId !== 'string') {
return;
}
});
}, []);
const trackPage = useCallback((path: string) => {
useAnalytics(ga => {
if (process.env.NODE_ENV === 'production') {
ga.pageview(path);
} else {
console.log(
`%cPage View %c${path}`,
'background: blue; color: white; padding: 0.5rem; font-size: 0.75rem;',
'background: white; color: blue; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
);
enabled.set(true);
const initializeOpts = { titleCase: false } as ReactGA.InitializeOptions;
if (debug) {
initializeOpts.debug = true;
}
});
}, []);
const trackModal = useCallback((path: string) => {
useAnalytics(ga => {
if (process.env.NODE_ENV === 'production') {
ga.modalview(path);
} else {
console.log(
`%cModal View %c${path}`,
'background: red; color: white; padding: 0.5rem; font-size: 0.75rem;',
'background: white; color: red; padding: 0.5rem; font-size: 0.75rem; font-weight: bold;',
);
}
});
}, []);
const initialize = useCallback((trackingId: string, debug: boolean) => {
if (typeof trackingId !== 'string') {
return;
}
enabled.set(true);
const initializeOpts = { titleCase: false } as ReactGA.InitializeOptions;
if (debug) {
initializeOpts.debug = true;
}
useAnalytics(ga => {
ga.initialize(trackingId, initializeOpts);
});
}, []);
runEffect(ga => {
ga.initialize(trackingId, initializeOpts);
});
},
[runEffect, enabled],
);
return { trackEvent, trackModal, trackPage, initialize, ga: ReactGA };
}

View file

@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { useConfig } from '~/context';
import { useGoogleAnalytics } from './useGoogleAnalytics';
@ -13,7 +13,7 @@ import type { LGQueryKey } from './types';
*/
export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryResponse> {
const { request_timeout, cache } = useConfig();
const controller = new AbortController();
const controller = useMemo(() => new AbortController(), []);
const { trackEvent } = useGoogleAnalytics();
@ -59,7 +59,7 @@ export function useLGQuery(query: TFormQuery): QueryObserverResult<TQueryRespons
() => () => {
controller.abort();
},
[],
[controller],
);
return useQuery<TQueryResponse, Response | TQueryResponse | Error, TQueryResponse, LGQueryKey>({

View file

@ -175,6 +175,7 @@ export function useLGState(): State<TLGState> {
export function useLGMethods(): TLGStateHandlers {
const state = useLGState();
state.attach(Methods);
// eslint-disable-next-line react-hooks/exhaustive-deps
const exporter = useCallback(Methods(state).stateExporter, [isEqual]);
return {
exportState(s) {

View file

@ -34,5 +34,5 @@ export function useOpposingColor(color: string, options?: TOpposingOptions): str
} else {
return options?.light ?? 'white';
}
}, [color]);
}, [isBlack, options?.dark, options?.light]);
}

View file

@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useCallback } from 'react';
import format from 'string-format';
import type { UseStrfArgs } from './types';
@ -6,6 +6,9 @@ import type { UseStrfArgs } from './types';
/**
* Format a string with variables, like Python's string.format()
*/
export function useStrf(str: string, fmt: UseStrfArgs, ...deps: unknown[]): string {
return useMemo(() => format(str, fmt), deps);
export function useStrf(): (str: string, fmt: UseStrfArgs, fallback?: string) => string {
return useCallback(
(str: string, fmt: UseStrfArgs, fallback?: string) => format(str, fmt) ?? fallback,
[],
);
}

View file

@ -102,8 +102,14 @@ export function useTableToString(
return result;
} catch (err) {
console.error(err);
return `An error occurred while parsing the output: '${err.message}'`;
let error = String(err);
if (err instanceof Error) {
error = err.message;
}
return `An error occurred while parsing the output: '${error}'`;
}
}
return useCallback(() => doFormat(target, data), deps);
const formatCallback = useCallback(doFormat, [target, data, doFormat]);
// eslint-disable-next-line react-hooks/exhaustive-deps
return useCallback(() => formatCallback(target, data), [target, data, formatCallback, ...deps]);
}

View file

@ -1,2 +1,6 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -11,9 +11,6 @@ module.exports = {
_HYPERGLASS_CONFIG_: config._HYPERGLASS_CONFIG_,
_HYPERGLASS_FAVICONS_: config._HYPERGLASS_FAVICONS_,
},
future: {
webpack5: true,
},
typescript: {
ignoreBuildErrors: true,
},

View file

@ -10,7 +10,7 @@
"dev": "node nextdev",
"start": "next start",
"typecheck": "tsc --noEmit",
"format": "prettier -c .",
"format": "prettier --config ./.prettierrc -c -w .",
"build": "next build && next export -o ../hyperglass/static/ui"
},
"browserslist": "> 0.25%, not dead",
@ -27,7 +27,7 @@
"dayjs": "^1.10.4",
"framer-motion": "^4.1.17",
"lodash": "^4.17.21",
"next": "^10.2.3",
"next": "^11.1.2",
"palette-by-numbers": "^0.1.5",
"react": "^17.0.2",
"react-countdown": "^2.2.1",
@ -53,27 +53,22 @@
"@types/react-select": "^4.0.15",
"@types/react-table": "^7.7.1",
"@types/string-format": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^4.11.1",
"@typescript-eslint/parser": "^4.11.1",
"@upstatement/eslint-config": "^0.4.3",
"@upstatement/prettier-config": "^0.3.0",
"@typescript-eslint/eslint-plugin": "^4.31.0",
"@typescript-eslint/parser": "^4.31.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^7.1.0",
"eslint-config-react-app": "^5.2.0",
"eslint-import-resolver-typescript": "^2.3.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.1.3",
"eslint-plugin-json": "^2.1.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.3.0",
"eslint-plugin-react": "^7.22.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-typescript": "^2.4.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-json": "^3.1.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.25.1",
"eslint-plugin-react-hooks": "^4.2.0",
"express": "^4.17.1",
"http-proxy-middleware": "0.20.0",
"onchange": "^7.1.0",
"prettier": "^2.2.1",
"prettier-eslint": "^12.0.0",
"typescript": "^4.3.2"
"prettier": "^2.3.2",
"prettier-eslint": "^13.0.0",
"typescript": "^4.4.2"
}
}

View file

@ -27,7 +27,7 @@ const App: NextApp<TApp> = (props: GetInitialPropsReturn<TApp>) => {
useEffect(() => {
router.events.on('routeChangeComplete', trackPage);
}, []);
}, [router.events, trackPage]);
return (
<>
@ -52,7 +52,7 @@ const App: NextApp<TApp> = (props: GetInitialPropsReturn<TApp>) => {
};
App.getInitialProps = async function getInitialProps() {
const config = (process.env._HYPERGLASS_CONFIG_ as unknown) as IConfig;
const config = process.env._HYPERGLASS_CONFIG_ as unknown as IConfig;
return { appProps: { config } };
};

View file

@ -30,7 +30,7 @@ const Index: React.FC<TIndex> = (props: TIndex) => {
};
export const getStaticProps: GetStaticProps<TIndex> = async () => {
const faviconConfig = (process.env._HYPERGLASS_FAVICONS_ as unknown) as Favicon[];
const faviconConfig = process.env._HYPERGLASS_FAVICONS_ as unknown as Favicon[];
const favicons = faviconConfig.map(icon => {
const { image_format, dimensions, prefix } = icon;
let { rel } = icon;

1681
hyperglass/ui/yarn.lock vendored

File diff suppressed because it is too large Load diff