forked from mirrors/thatmattlove-hyperglass
Update next, eslint, fix lint errors
This commit is contained in:
parent
5ccfe50792
commit
eff99ad294
46 changed files with 1051 additions and 1189 deletions
|
|
@ -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
13
hyperglass/TODO.md
Normal 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
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
69
hyperglass/ui/.eslintrc.js
Normal file
69
hyperglass/ui/.eslintrc.js
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -14,5 +14,5 @@ export function useBooleanValue<T extends unknown, F extends unknown>(
|
|||
} else {
|
||||
return ifFalse;
|
||||
}
|
||||
}, [status]);
|
||||
}, [status, ifTrue, ifFalse]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>({
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -34,5 +34,5 @@ export function useOpposingColor(color: string, options?: TOpposingOptions): str
|
|||
} else {
|
||||
return options?.light ?? 'white';
|
||||
}
|
||||
}, [color]);
|
||||
}, [isBlack, options?.dark, options?.light]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
4
hyperglass/ui/next-env.d.ts
vendored
4
hyperglass/ui/next-env.d.ts
vendored
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ module.exports = {
|
|||
_HYPERGLASS_CONFIG_: config._HYPERGLASS_CONFIG_,
|
||||
_HYPERGLASS_FAVICONS_: config._HYPERGLASS_FAVICONS_,
|
||||
},
|
||||
future: {
|
||||
webpack5: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
|
|
|||
35
hyperglass/ui/package.json
vendored
35
hyperglass/ui/package.json
vendored
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 } };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
1681
hyperglass/ui/yarn.lock
vendored
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue